[OpenGL] 그림자 최적화

gest·2026년 5월 2일

OpenGL

목록 보기
3/11

전 게시물에서는 그림자에 대해 배웠다. 그러나 만약 맵에 있는 모든 fragment의 그림자를 구현한다면 어떻게 될까? 맵이 넓다면 심한 과부화가 생길 것이다.
그래서 이를 최적화하기 위해, 현재 내 카메라 시야(화면) 주변에만 집중적으로 그림자를 계산하는 기술을 구현해 볼 예정이다.
참고로 이 알고리즘은 FSM (Frustum-fitted Shadow Mapping), 즉 시야 기반 그림자 매핑이다.


(A)

A는 라이트 공간 행렬을 구하는 식이라고 보면 된다.
나중에 공간 전환할때 사용된다.

// (A) 박스 중심: 카메라 앞쪽으로 radius만큼 떨어진 지점 (그래야 시야 안에 그림자가 다 들어감)
glm::vec3 center = cam.Position + cam.Front * radius;
glm::mat4 lightView = glm::lookAt(center - normal_light, center, glm::vec3(0.0f, 1.0f, 0.0f));

(B) 텍셀 스냅

빛의 카메라(박스 중심)를 부드럽게 이동시키지 않고, 그림자 맵의 격자(텍셀) 크기 단위로 딱딱 끊어서 이동하도록 맞추는 과정이다.
즉 애매하게 12.5에 위치하면 12로 버림하는 과정이라고 생각하면 된다.

이걸 왜 하는가?

만약 그대로 두는 경우 카메라가 조금만 움직여도 그림자가 렌더링하는 과정에서 문제가 생긴다.
픽셀들의 위치가 매 프레임 달라지고 그림자 외각선이 꿈뜰거리게 된다.

그래서 이 현상을 방지하기 위해 소수점 좌표를 버리는 기술을 텍셀 스냅이라고 한다.
여담으로 센터를 중심으로 그림자맵 격자를 맞춘다고 보면 된다.

float worldUnitsPerTexel = (2.0f * radius) / (float)shadowMapSize; //1픽셀에 몇 m인지 => 스크린 사이즈
glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f); //라이트 공간으로 전환
centerLS.x = std::floor(centerLS.x / worldUnitsPerTexel) * worldUnitsPerTexel; //12.5면 12로 스크린 사이즈 단위로 변환
centerLS.y = std::floor(centerLS.y / worldUnitsPerTexel) * worldUnitsPerTexel;
glm::vec3 snappedCenter = glm::vec3(glm::inverse(lightView) * centerLS); //다시 현실 공간으로 전환

(C)

//   z 범위는 그림자 캐스터가 박스 뒤쪽에 있어도 잡히도록 넉넉하게
constexpr float zRange = 100.0f;
glm::mat4 lightProjection = glm::ortho(-radius, radius, -radius, radius, -zRange, zRange);

//빛 뷰(lightView)로 전환 후 정해진 규격(직육면체)로 전환
return lightProjection * lightView;


코드

// 카메라 범위만큼만 그림자 계산 => 최적화를 위한 기술
static glm::mat4 ComputeLightSpaceMatrix(const Camera& cam, const glm::vec3& lightDir,
                                         float radius, unsigned int shadowMapSize)
{
    //light의 노멀벡터
    glm::vec3 normal_light = glm::normalize(lightDir);

    // (A) 박스 중심: 카메라 앞쪽으로 radius만큼 떨어진 지점 (그래야 시야 안에 그림자가 다 들어감)
    glm::vec3 center = cam.Position + cam.Front * radius;
    glm::mat4 lightView = glm::lookAt(center - normal_light, center, glm::vec3(0.0f, 1.0f, 0.0f));

    // (B) 텍셀 스냅: 박스 중심을 그림자맵 텍셀 격자에 맞춤
    //   - 라이트 공간에서 텍셀 한 칸의 월드 크기 = (2 * radius) / shadowMapSize
    //   - 중심을 라이트 공간으로 옮긴 뒤, 그 좌표를 텍셀 단위로 floor → 다시 월드로
    float worldUnitsPerTexel = (2.0f * radius) / (float)shadowMapSize;
    glm::vec4 centerLS = lightView * glm::vec4(center, 1.0f);
    centerLS.x = std::floor(centerLS.x / worldUnitsPerTexel) * worldUnitsPerTexel;
    centerLS.y = std::floor(centerLS.y / worldUnitsPerTexel) * worldUnitsPerTexel;
    glm::vec3 snappedCenter = glm::vec3(glm::inverse(lightView) * centerLS);

    // 스냅된 중심으로 lightView 다시 만들기
    lightView = glm::lookAt(snappedCenter - normal_light, snappedCenter, glm::vec3(0.0f, 1.0f, 0.0f));

    // (C) ortho는 항상 같은 크기 (-radius ~ +radius)
    //   z 범위는 그림자 캐스터가 박스 뒤쪽에 있어도 잡히도록 넉넉하게
    constexpr float zRange = 100.0f;
    glm::mat4 lightProjection = glm::ortho(-radius, radius, -radius, radius, -zRange, zRange);

    return lightProjection * lightView;
}

0개의 댓글