본문으로 바로가기

6장 3D Graphics - ③ 3D 메시 그리기

6장에서는 2D 환경에서 완전한 3D 게임으로 전환하는 방법을 다룬다. 3D 게임으로 전환하기 위해서는 먼저 3D회전을 포함한 엑터의 변환을 다룰 수 있어야 한다. 또한 3D 모델을 로드하고 그릴 수 있어야 한다. 마지막으로 대부분의 3D 게임 장면에서 제공하는 여러 타입의 조명을 구현해야 한다.

💡 6장의 목차

  • 3D에서 물체의 변환과 쿼터니언
  • 3D 모델 로딩하기
  • 3D 메시 그리기
  • 조명

3D 메시 그리기

이전 장에서 3D 모델을 로딩했으니 이번에는 그려볼 차례이다. 이제까지는 2D 물체를 화면에 출력해왔었다. 렌더링 과정을 다시 짚어보면 다음과 같다. 먼저 지금까지 우리가 2D 물체를 출력하기 위해서 지금까지 우리가 사용한 렌더링 과정을 도식화 하면 다음과 같다.

▲ 2D Rendering Pipeline

2D 렌더링 과정에서 주목할 만한 점은 다음과 같다. (2D 렌더링에 대한 추가적인 설명은 여기를 참고하자.)

  • 2개의 삼각형으로 스프라이트 버텍스를 만들어서 모든 스프라이트를 출력한다.
  • objec space와 world space가 2D상태이기 때문에 간단하게 2D 좌표계에 해당하는 상태의 clip space로 간단하게 변환할 수 있었다.

3D 물체를 표현하기 위해서는 이전 글에서 다뤘듯이 많은 폴리곤으로 이뤄진 메시들을 사용한다. 그리고 3D 공간 좌표를 2D 상으로 변환해야하고, 앞으로 조명도 다뤄야 한다. 그렇기 때문에 아래와 같이 조금 더 복잡한 렌더링 과정을 가진다.

▲ 3D Rendering Pipeline

2차원 평면 물체를 평면으로 출력하는것과 다르게 3차원 물체를 2차원 평면으로 변환하여 출력하는 과정에서는 뷰변환과 투영(Projction)변환이 조금 더 복잡해진다. 2D에서는 SimpleViewProjection 하나로 사용하던 뷰 행렬과 투영 행렬을 두 개로 세분화 한다.

뷰 행렬

뷰공간은 월드상에 있는 물체들을 카메라(유저)의 관점에서 물체를 바라보는 공간이다. 위 도표에서의 View Space는 원기둥과 직육면체를 정면에서 바라보는 시점을 표현한 것이다. 때문에 카메라의 위치와 바라보는 방향등에 따라서 View Space의 구성은 변한다. 우리는 look-at 행렬을 통해서 카메라의 위치와 방향을 나타낼 수 있다. look-at 행렬의 구성요소는 다음과 같다.

  • 눈의 위치
  • 눈이 바라보는 타깃의 위치
  • 카메라의 위쪽 방향

look-at 행렬이 바로 view 변환 행렬이 된다. 즉 오브젝트에 view 변환 행렬을 곱하면 물체의 좌표가 카메라의 관점에서 물체의 위치로 변경된다.(실제 물체가 변경되는 것이 아니라 원점의 기준이 변경되는 것이다.)

교재에서 제공해준 Math.hMatrix::CreateLookAt() 함수를 통해서 LookAt 행렬을 만들어낼 수 있다.

Matrix4 view = Matrix4::CreateLookAt(eye, target, Vector3::UnitZ)

투영 행렬

투영 행렬(Projection matrix)은 3차원 물체를 2차원 평면에 투영시켜주는 변환 행렬이다. 뷰 공간은 월드 공간을 어디서 바라볼지 결정했을 뿐 아지 3차원 물체들로 구성되어 있다. 이 3차원 뷰공간을 2차원 클립공간(NDC)좌표계로 변경하기 위해서 투영 행렬을 사용한다. 투영 행렬에는 직교 투영(Orthographic projection)과 원근 투영(Perspective projection)이 존재한다.

직교 투영 (Orthographic projection)

직교 투영의 핵심은 카메라와 물체사이의 거리와 상관없이 물체의 크기가 동일하다는 특징을 가진다. 즉 멀리 있더라도 크기가 작아지지 않는다. 보통 실제 크기를 가늠할 수 있어야 하는 건축 도면에서 많이 사용한다.

▲ 직교 투영 예시

직교 투영에는 가까운 평면과 먼 평면과 평면의 높이 너비가 있다. 이 4개의 값으로 아래의 그림처럼 view volum을 형성하고, 이 공간 밖의 물체는 클립한다(잘라낸다).

▲ 직교 투영 클리핑

직교 투영 행렬을 만들어 내기위해서는 다음 4개를 파라미터로 가진다.

  • 뷰의 너비
  • 뷰의 높이
  • 카메라 에서 가까운 평면과의 거리
  • 카메라 에서 먼 평면과의 거리

이 파라미터를 활용해서 다음과 같이 직교 투영 변환 행렬을 만들어 낼 수 있다.

Math.h의 중

    static Matrix4 CreateOrtho(
        float width,  // 뷰의 너비 
        float height, // 뷰의 높이
        float near,   // 가까운 평면까지 거리
        float far);   // 먼 평면까지 거리    

원근 투영 (Perspective projection)

원근 투영은 일상에서 우리가 자주 경험하는 원근감을 표현하는 변환 방법이다. 원근법으로 인해서 실제로는 도로처럼 평행하던 직선이 한 점에서 만나는것 처럼 보이게 된다. 많은 3D 게임에서도 현실감을 위해서 원근 투영을 사용한다.

원근 투영에는 직교투영이 가지고 있는 데이터에 추가적으로 수평시야각FOV(horizontal Field Of View)라 불리는 파라미터를 가진다. fov가 크다면 더 넓은 시야를 가질 수 있지만, 원근 효과는 더욱 커지게 된다. 보통 45도로 설정하지만 둠 스타일 결과를 원한다면 좀 더 높은 값으로 설정 할 수 있다.

▲ 원근 투영 클리핑

직교 투영 행렬을 만들어 내기위해서는 다음 5개를 파라미터로 가진다.

  • 수평 시야각 FOV
  • 뷰의 너비
  • 뷰의 높이
  • 카메라 에서 가까운 평면과의 거리
  • 카메라 에서 먼 평면과의 거리

이 파라미터를 활용해서 다음과 같이 원근 투영 변환 행렬을 만들어 낼 수 있다.

Math.h의 중

    static Matrix4 CreatePerspectiveFOV(
        float fovY,   // 수평시야각
        float width,  // 뷰의 너비 
        float height, // 뷰의 높이
        float near,   // 가까운 평면까지 거리
        float far);   // 먼 평면까지 거리   

Z-버퍼링

그동안 2D 스프라이트를 그릴때는 화가알고리즘을 사용했다. 3D 물체에서는 다음과 같은 이유로 화가 알고리즘을 사용하기는 어렵다.

화가 알고리즘의 문제

  • 앞뒤 정렬 순서가 정적이지 않다. 액터의 이동, 카메라의 이동으로 물체의 앞뒤 순서는 얼마든지 변할 수 있다. 이때마다 정렬을 통해 그리기 순서를 정할시 병목현상 발생
  • 불필요한 그리기 연산을 수행한다. 프레임 마다 단일 픽셀에 여러 번 색상을 덮어쓰는 문제를 야기한다. 3D 렌더링에서 픽셀의 최종 색상을 계산하는 과정은 렌더링 파이프라인과정 중 가장 비싼 비용이 든다.
  • 삼각형이 겹치는 문제가 발생한다. 아래의 그림과 같은 이미지는 화가 알고리즘으로는 그리기 어렵다.(이를 화가알고리즘으로 그릴려면 삼각형을 분할 해야만 한다.)

▲ 화가 알고리즘의 문제 (출처: 위키 피디아)

Z-버퍼링 도입

Z 버퍼링 렌더링 과정 동안 각 픽섹에 대한 깊이정보를 담을 메모리 버퍼를 추가로 사용한다. 때문에 Z버퍼라고 부르기도하고 깊이 버퍼라고 부르기도 한다. 각 픽셀의 깊이는 카메라와 거리를 통해서 계산한 정규화(1.0f ~ 0.0f)된 값이다. z버퍼는 매 프레임마다 다음의 순서로 동작한다.

  • 모든 픽셀의 z버퍼를 최대 값 1.0f (최대 깊이)로 초기화 한다.
  • 각 픽셀마다 기존의 z버퍼와 그리려는 픽셀의 z버퍼를 비교한다.
  • 그리려는 z버퍼가 더 작을 경우만 그린다.

Z-버퍼의 사용

Z-버퍼와 관련된 작업은 OpenGL에서 모두 지원하기 때문에 개발자는 단지 Z버퍼링을 활성화/비활성화 만 해주면 된다. z버퍼를 사용하기 위해서는 먼저 context를 생성하기 전에 깊이 버퍼를 요청해야한다.

// 깊이 버퍼 사용 요청
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

그리고 깊이 버퍼를 활성화한다.

glEnable(GL_DEPTH_TEST);

glClear함수를 통해서 매 프레임마다 다음과 같이 색상 버퍼와 함께 깊이 버퍼를 초기화해야 한다.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Z-버퍼의 문제점

  • 3D 오브젝트가 투명도를 가질 경우: (물과 같은) 반투명 물체 뒤에 있는 (돌과 같은) 불투명 물체를 그릴 때 문제가 생긴다. 반투명 물체가 먼저 그려진다면, 반투명 물체 뒤에 있는 불투명 물체는 보이지 않게 된다.
    • 이 문제를 해결하기 위해서는 z버퍼를 활성화 한 후 불투명 물체를 그리고 z버퍼를 비활성화 하고 반투명 물체를 그린다.
    • 특히나 2차원 스프라이트에서는 알파블렌딩을 많이 사용한다. 때문에 3차원 게임에서 UI와 같은 2차원 스프라이트를 그리기 위해서는 z버퍼를 활성화 한 후 3D 물체를 그리고, z버퍼를 끄고 2차원 스프라이트를 그리는 방식으로 문제를 해결할 수 있다.

출처: