Computer Graphics/ShaderPixel

ShaderPixel - 14. 카메라 기준으로 오브젝트 렌더링 순서 정리하기

surkim 2024. 11. 24. 14:51

오브젝트를 카메라 기준으로 멀리서부터 그리는 방식을 구현하였다. 이는 일부 오브젝트, 특히 CloudWater Block과 같은 효과를 위해 필요했다.


왜 멀리서부터 그려야 하는가?

다른 대부분의 오브젝트는 Vertex Shader에서 미리 정해진 메쉬를 통해 그릴 공간을 정하고, Fragment Shader에서 Ray Marching 방식으로 렌더링한다. 이런 방식은 프레임버퍼의 뎁스 값이 이미 Vertex Shader 단계에서 처리되므로 어떤 순서로 그려도 문제가 없다. 또한, 오브젝트들이 전시 형태로 멀리 떨어져 있어 순서의 영향을 크게 받지 않는다.

그러나 CloudWater Block은 조금 다르다.

Cloud

  • 구름의 끝부분이 매끄럽게 처리되지 않았다.
  • 배경과 어우러지도록 렌더링해야 하는데, 현재 그리는 시점에서는 배경에 어떤 내용이 있는지 알 수 없어 검은색 픽셀이 나타나는 문제가 있었다.

Water Block

  • Water Block은 빛의 굴절 효과를 구현하기 위해 뒤에 있는 물체를 참고해야 한다.
  • 같은 프레임버퍼에 그린다면, 해당 프레임버퍼를 참고할 수 없으므로 뒤에 있는 물체를 기반으로 굴절 효과를 계산할 수 없다.

 

이러한 문제를 해결하기 위해 이미 그려진것을 프레임버퍼에 저장하고 cloud와 water block은 그 위에다 그리는 방법을 선택했었다. 이렇게 되면 생기는 문제가 있는데 바로 위의 사진처럼 구름이나 water block이 나중에 그려졌을때 뒤에있더라도 앞에 보이게 된다는 점이다. 깊이 계산을 통하지 않고 그냥 사진위에 그리는 방식을 택했기 때문.. 

 

그래서 카메라에서 멀리 있는 오브젝트부터 렌더링하는 방식을 선택하였다.
더 나은 방법으로, 예를 들어 프레임버퍼에 직접 뎁스 값을 계산하여 한 번에 처리하는 방식도 있을 수 있다. 그러나 시간과 기술적 한계로 인해 이번 프로젝트에서는 렌더링 순서를 조정하는 방법으로 문제를 해결하였다.


구현 방법

1. 렌더링 순서를 관리할 구조체

enum ObjectType {
    BEAD,
    MANDELBOX,
    MANDELBULB,
    SPONGE,
    WORLD,
    KALEIDOSCOPE,
    CLOUD,
    WATER
};

struct DrawCall {
    int type;
    glm::vec3 pos;
    float distance = 0;
};
  • ObjectType 열거형(enum)은 각 오브젝트의 타입을 정의하였다.
  • DrawCall 구조체는 오브젝트의 타입과 위치, 그리고 카메라와의 거리를 저장한다. 이 정보를 기반으로 렌더링 순서를 결정한다.

2. 각 오브젝트의 렌더링 함수

void DrawBead(const glm::mat4& projection, const glm::mat4& view);
void DrawMandelbox(const glm::mat4& projection, const glm::mat4& view);
void DrawMandelbulb(const glm::mat4& projection, const glm::mat4& view);
void DrawSponge(const glm::mat4& projection, const glm::mat4& view);
void DrawAnotherWorld(const glm::mat4& projection, const glm::mat4& view);
void DrawKaleidoscope(const glm::mat4& projection, const glm::mat4& view);
void DrawCloud(const glm::mat4& projection, const glm::mat4& view);
void DrawWater(const glm::mat4& projection, const glm::mat4& view);
  • 각각의 오브젝트를 렌더링하는 별도의 함수를 구현하였다.
  • 각 함수는 동일한 projectionview 행렬을 입력으로 받는다.

3. 카메라 기준 거리 계산 및 정렬

void Context::CalDistance() {
    for (int i = 0; i < 8; i++) {
        m_drawcalls[i].distance = glm::length(m_cameraPos - m_drawcalls[i].pos);
    }
}

void Context::SortDrawCall() {
    std::sort(m_drawcalls, m_drawcalls + 8, [](const DrawCall& a, const DrawCall& b) {
        return a.distance > b.distance;
    });
}
  • CalDistance: 카메라 위치(m_cameraPos)와 오브젝트 위치 간의 거리를 계산하여 DrawCall 구조체에 저장.
  • SortDrawCall: 거리에 따라 DrawCall 배열을 정렬. 멀리 있는 오브젝트가 먼저 그려지도록 구성하였다.

4. 모든 오브젝트 렌더링

void Context::DrawAll(const glm::mat4& projection, const glm::mat4& view) {
    for (int i = 0; i < 8; i++) {
        switch (m_drawcalls[i].type) {
        case BEAD:
            DrawBead(projection, view);
            break;
        case MANDELBOX:
            DrawMandelbox(projection, view);
            break;
        case MANDELBULB:
            DrawMandelbulb(projection, view);
            break;
        case SPONGE:
            DrawSponge(projection, view);
            break;
        case WORLD:
            DrawAnotherWorld(projection, view);
            break;
        case KALEIDOSCOPE:
            DrawKaleidoscope(projection, view);
            break;
        case CLOUD:
            DrawCloud(projection, view);
            break;
        case WATER:
            DrawWater(projection, view);
            break;
        }
    }
}
  • m_drawcalls 배열을 순회하며 각 오브젝트를 렌더링.
  • 각 오브젝트의 타입에 따라 해당 렌더링 함수를 호출한다.

구현 결과

이랬던 금쪽이들이
이렇게 뒤로 잘 그려진다.
이건 이뻐서 추가

  • Cloud와 Water Block 렌더링 문제를 해결하였다.
  • 이제 구름과 물 블록이 다른 오브젝트 위에 그려지는 문제가 발생하지 않는다.

대안에 대한 고민

  • 이번 구현에서는 렌더링 순서를 조정하여 문제를 해결했으나, 더 나은 대안이 있을 수 있다.
    예를 들어, 프레임버퍼에 뎁스 값을 직접 계산하여 처리하거나, 멀티패스 렌더링을 활용할 수도 있다.
  • 그러나 현재 프로젝트의 제한된 시간과 목적을 고려하여 간단한 방식으로 문제를 해결하였다.

 


 

이로써 ShaderPixel 과제가 완료되었다. 이번 과제는 12월부터 시작될 팀 프로젝트로 인해 다소 빠르게 마무리되었지만, 다음 프로젝트인 Vulkan을 활용한 게임 엔진 개발이 더 흥미로워 보이기에 적절한 판단이었다고 생각한다.

과제를 통해 셰이더 프로그래밍을 보다 심도 있게 다룰 수 있었고, 프레임버퍼를 활용하는 방법을 익힌 점도 큰 성과였다. 이번 프로젝트 경험은 앞으로 진행될 프로젝트에서 셰이더와 관련된 작업을 보다 효과적으로 수행하는 데 많은 도움이 될 것이다.

다음 블로그 포스팅에서는 이번 과제의 결과를 시연한 동영상이미지를 첨부하며 글을 마무리할 예정이다.