Computer Graphics/ShaderPixel

ShaderPixel - 1. 배경 구성 (Skybox)

surkim 2024. 11. 11. 11:34

이번 작업에서는 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 이미지를 스카이박스로 렌더링하여 배경에 고정하는 과정이다. 카메라의 projectionview 행렬을 통해 깊이 값을 조절하여 스카이박스가 항상 배경으로 보이도록 설정했다.

// 깊이 테스트를 설정하여 스카이박스가 항상 배경에 위치하도록 조정
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의 색감과 밝기가 화면에 자연스럽게 표시되도록 했다.


결과

0