이번 작업에서는 HDR 파노라마 이미지를 큐브맵 텍스처로 변환해 스카이박스(Skybox)를 구성했다.
캡처 초기화 코드
HDR 이미지를 불러온 후, 큐브맵 텍스처로 변환하기 위해 큐브 프레임버퍼와 뷰 행렬을 설정했다.
// HDR 이미지를 로드하여 Texture 객체로 생성
m_hdrMap = Texture::CreateFromImage(Image::Load("./image/god_rays_sky_dome_8k.hdr").get());
// 스피어리컬 맵에서 큐브맵으로 변환하는 셰이더 프로그램 로드
m_sphericalMapProgram = Program::Create("./shader/spherical_map.vs", "./shader/spherical_map.fs");
// 큐브맵 텍스처 생성 (2048x2048 크기, HDR 색상 포맷)
m_hdrCubeMap = CubeTexture::Create(2048, 2048, GL_RGB16F, GL_FLOAT);
// 큐브 프레임버퍼 생성
auto cubeFramebuffer = CubeFramebuffer::Create(m_hdrCubeMap);
// 큐브맵 면별로 90도 각도로 설정된 뷰 행렬 구성
auto projection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f);
std::vector<glm::mat4> views = {
glm::lookAt(glm::vec3(0.0f), glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)),
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)),
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
glm::lookAt(glm::vec3(0.0f), glm::vec3(0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)),
};
// 셰이더 프로그램 사용 및 텍스처 바인딩
m_sphericalMapProgram->Use();
m_sphericalMapProgram->SetUniform("tex", 0);
m_hdrMap->Bind();
// 큐브맵을 2048x2048 크기로 뷰포트 설정 후 변환
glViewport(0, 0, 2048, 2048);
for (int i = 0; i < (int)views.size(); i++) {
// 큐브맵 텍스처의 특정 면을 대상으로 하는 프레임버퍼를 바인딩
cubeFramebuffer->Bind(i);
// 색상 버퍼와 깊이 버퍼를 초기화
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 각 면에 해당하는 투영 및 뷰 행렬을 셰이더로 전달
m_sphericalMapProgram->SetUniform("transform", projection * views[i]);
// 큐브의 면을 그려, 스피어리컬 맵을 큐브맵 텍스처로 변환
m_box->Draw(m_sphericalMapProgram.get());
}
// 기본 프레임버퍼로 전환하여 초기 뷰포트 복구
Framebuffer::BindToDefault();
glViewport(0, 0, m_width, m_height);
HDR 캡처 셰이더 코드
HDR 이미지를 큐브맵으로 캡처하기 위한 Vertex와 Fragment 셰이더 코드이다. 각 큐브맵 면에 HDR 이미지가 정확히 매핑되도록 한다.
Vertex Shader (spherical_map.vs
)
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 localPos;
uniform mat4 transform;
void main() {
gl_Position = transform * vec4(aPos, 1.0); // 캡처 면에 대한 변환 적용
localPos = aPos; // Fragment Shader로 전달할 로컬 좌표
}
Fragment Shader (spherical_map.fs
)
#version 330 core
out vec4 fragColor;
in vec3 localPos;
uniform sampler2D tex;
const vec2 invPi = vec2(0.1591549, 0.3183098862);
vec2 SampleSphericalMap(vec3 v) {
return vec2(atan(v.z, v.x), asin(v.y)) * invPi + 0.5; // 스피어리컬 맵 샘플링 함수
}
void main() {
vec2 uv = SampleSphericalMap(normalize(localPos)); // 로컬 좌표를 정규화하여 스피어리컬 맵 적용
vec3 color = texture(tex, uv).rgb; // HDR 텍스처 샘플링
fragColor = vec4(color, 1.0); // 결과 색상 출력
}
이 셰이더는 HDR 이미지의 스피어리컬 좌표를 큐브맵의 각 면에 매핑하기 위해 SampleSphericalMap
함수를 사용한다. 이 함수는 HDR 텍스처를 정확히 큐브맵 면에 매핑할 수 있도록 스피어리컬 좌표를 생성해준다.
2. 큐브맵 렌더링
큐브맵으로 생성된 HDR 이미지를 스카이박스로 렌더링하여 배경에 고정하는 과정이다. 카메라의 projection
및 view
행렬을 통해 깊이 값을 조절하여 스카이박스가 항상 배경으로 보이도록 설정했다.
// 깊이 테스트를 설정하여 스카이박스가 항상 배경에 위치하도록 조정
glDepthFunc(GL_LEQUAL);
// 스카이박스 셰이더 프로그램 사용
m_skyboxProgram->Use();
m_skyboxProgram->SetUniform("projection", projection); // 프로젝션 행렬을 셰이더로 전달
m_skyboxProgram->SetUniform("view", view); // 뷰 행렬을 셰이더로 전달 (위치 정보는 제외)
m_skyboxProgram->SetUniform("cubeMap", 0); // 스카이박스 큐브맵 텍스처 유니폼 설정
// 큐브맵 텍스처를 바인딩하여 스카이박스에 사용
m_hdrCubeMap->Bind();
// 스카이박스 큐브를 화면에 그려 배경으로 적용
m_box->Draw(m_skyboxProgram.get());
// 깊이 테스트를 원래대로 되돌려 다른 객체들이 스카이박스 위에 렌더링되도록 설정
glDepthFunc(GL_LESS);
위 코드에서 glDepthFunc(GL_LEQUAL);
을 사용하는 이유는 스카이박스가 항상 배경으로 보이도록 깊이 테스트를 조정하기 위함이다. GL_LEQUAL
은 깊이 값이 현재 깊이 버퍼의 값보다 작거나 같은 픽셀만 렌더링되도록 하는 설정이다. 스카이박스의 깊이 값이 항상 1.0(가장 뒤)에 있기 때문에, GL_LEQUAL
을 설정하면 스카이박스가 배경으로 고정된 상태에서 렌더링된다.
스카이박스 렌더링 셰이더 코드
스카이박스를 화면에 렌더링하기 위한 Vertex와 Fragment 셰이더 코드로, HDR의 넓은 색상 범위를 조정하기 위해 Reinhard 톤 매핑과 감마 보정을 추가했다.
Vertex Shader (skybox.vs
)
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 localPos;
uniform mat4 projection;
uniform mat4 view;
void main() {
localPos = aPos; // 큐브맵 텍스처 좌표로 사용될 로컬 좌표
mat4 rotView = mat4(mat3(view)); // 뷰 행렬에서 위치 정보 제거 (회전만 반영)
vec4 clipPos = projection * rotView * vec4(localPos, 1.0); // 최종 변환 적용
gl_Position = clipPos.xyww; // 깊이 고정하여 스카이박스를 배경에 고정
}
Fragment Shader (skybox.fs
)
#version 330 core
out vec4 fragColor;
in vec3 localPos;
uniform samplerCube cubeMap;
void main() {
vec3 envColor = texture(cubeMap, localPos).rgb; // 큐브맵 텍스처 샘플링
envColor = envColor / (envColor + vec3(1.0)); // Reinhard 톤 매핑
envColor = pow(envColor, vec3(1.0/2.2)); // 감마 보정 (sRGB 변환)
fragColor = vec4(envColor, 1.0); // 최종 색상 출력
}
여기서는 HDR 색상 범위를 조정하기 위해 Reinhard 톤 매핑(envColor / (envColor + vec3(1.0))
)을 사용하고, 감마 보정으로 sRGB 변환을 추가했다. 이렇게 함으로써 HDR의 색감과 밝기가 화면에 자연스럽게 표시되도록 했다.
결과
'Computer Graphics > ShaderPixel' 카테고리의 다른 글
ShaderPixel - 5. 색 구슬 구현 (0) | 2024.11.17 |
---|---|
ShaderPixel - 4. 버텍스 셰이더에서 계산한 노말 값과 월드 좌표는 실제로 프래그먼트에 그려지는 픽셀의 월드 좌표와는 다르다 (2) | 2024.11.15 |
ShaderPixel - 3. 렌더링 전략 고민.. (1) | 2024.11.13 |
ShaderPixel - 2. 바닥 구성 (Normal map) (0) | 2024.11.11 |
ShaderPixel - 0. 과제 해석 (0) | 2024.10.30 |