본문으로 바로가기

6장 3D Graphics - ④ 조명(퐁 셰이딩 - Phong Shading)

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

💡 6장의 목차

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

조명

모든 3차원 물체에는 조명으로 인해서 밝고 어두운 부분 즉, 음영이 생긴다. 조명과 음영은 물체의 입체감과 부피감을 묘사하는 데 큰 도움이 된다. 인간의 시각적 인식은 빛과 물체 재질의 상호작용에 의존하며, 따라서 실사적인 장면을 생성하는 문제의 상당 부분은 물리적으로 정확한 조명 모형과 관련이 있다. 아래의 그림에서 왼쪽은 조명이 없는 구이고, 오른쪽은 조명이 비춰진 구이다. 조명이 없는 왼쪽 구는 3차원 구가 아니라 2차원 원으로 보이기도 한다. 이렇게 조명과 음영의 표현은 물체의 입체감을 표현하는데 중요한 역할을 한다.

▲ 조명과 음영을 통한 입체감 표현

법선 벡터

법선 벡터란 다각형이 향하고 있는 방향을 나타내는 단위이다. 우리의 눈이 물체를 인식할 수 있는 이유는 물체에서 반사된 빛이 우리의 눈으로 들어와서 시신경을 자극하기 때문이다. 3D 그래픽에서 조명을 구현하기 위해서는 이 반사되는 빛을 계산 할 수 있어야 한다. 빛이 물체에 일정 각도로 비춰지게 되는데 이 각을 입사각이라고 한다. 그리고 빛이 물체의 표면에서부터 반사(혹은 흡수)가 되는데 이때 반사되어서 나오는 각을 반사각 이라고 한다. 그리고 입사각과 반사각은 항상 물체 표면의 법선 벡터에 대칭이기 때문에 조명을 계산하기 위해서 법선 벡터는 필수적이다.

버텍스 법선

법선은 표면에 수직이다. 하지만 우리는 버텍스의 정보를 통해서 표면을 만든다. 때문에 (이 글에서 언급한 대로)버텍스의 정보에 법선벡터를 포함했다. 그리고 인덱스 버퍼를 통해서 서로 다른 평면이 동일한 버텍스를 공유하는 방식을 사용해왔다. 서로 다른 평면이라면 법선 벡터도 서로 다를 수 있지만, 동일한 버텍스를 서로 공유할 경우 제대로된 법선 벡터를 입력할 수 없다. 그래서 동일한 위치의 버텍스라고 하더라도 법선 벡터가 다르다면 다른 버텍스로 봐야한다.

▲ 버텍스 법선

법선 벡터의 보간

필요에 따라서 같은 삼각형이라 하더라도 각 정점의 법선 벡터가 다를 수 있다. 곡면을 표현하는 삼각형일 때 이런 경우가 많다. 그렇다면 버텍스별 서로 다른 법선 벡트를 활용해서 삼각형 전체에 걸쳐서 법선 벡터를 보간해야한다. 프래그먼트 셰이더로 버텍스를 보내면 버텍스 속성이 삼각형 전체에 걸쳐 보간됐던 것을 상기하자. 법선벡터도 마찬가지로 삼각형 전체에서 아래의 그림과 같이보간 작업이 일어난다. 이때는 아래의 수식처럼 선형 보간기법을 사용한다.
\[n = n_0 + t(n_1 - n_0)\]

▲ 법선 벡터의 보간

광원의 유형

현실세계의 빛의 다양한 유형을 수학적으로 모두 계산 하는것은 시간이 오래 걸리는 일이다. 영화와 같이 1초의 영상을 렌더링하기 위해서 한시간이 넘게 걸려도 상관없다면, 최대한 현실적으로 조명을 계산할 수 있다. 하지만 우리는 실시간 3D 영상을 렌더링해야한다. 때문에 3D 그래픽스에서는 조명 계산을 단순화하는 많은 방법들을 사용한다. 그중에서 여기서는 조명을 표현하기 위한 하나의 기법인 퐁셰이딩에 사용된 몇 가지 광원만 다룬다.

주변광 조명

현실에서 주전자의 앞면에 빛을 비추더라도 주전자의 뒷면이 완전히 검지는 않다. 그 이유는 벽이나 다른 물체에 빛이 2차 반사되어서 주전자 뒷면을 비추기 때문이다. 그리고 현실에서는 이런 2차로 반사된 빛 뿐만 아니라 그 이상으로 반사된 빛들이 존재한다. 하지만, 이 모든 간접 조명을 계산하는 비용은 결코 저렴하지 않다. 그래서 이를 단순화 하기 위해서 주변광이라는 개념을 사용한다.
주변광은 장면에서 모든 단일 오브젝트에 적용되는 균일한 양의 빛이다. 동일한 게임의 레벨이라면 동알한 주변광을 적용할 수 있을 것이다. 주변광은 균일한 양을 제공하므로 오브젝트의 여러 면을 다르게 비춰주지 않는다.

분산광 조명

분산광은 거친 표면에서 분산 반사 혹은 난반사가 발생하면서 발생한 빛이다. 분산광의 특징은 빛이 투영된 표면을 어느 위치에서 보나 동일한 크기와 색상의 빛을 가진다는 것이다. 스크린에 투영되는 빔프로젝트 화면이 대표적인 분산광 조명이다. 만약 스크린이 거친 표면의 흰색이 아니라 거울과 같이 매끈한 표면이었다면 화면을 바라보는 사람의 자리에 따라서 화면의 밝기가 다르게 보일 것이다.

반영광 조명

반영광은 매끄러운 표면에서 발생하는 정반사되면서 발생한 빛이다. 정반사된 반영광은 방향성을 지닌 빛이다. 때문에 관찰자가 이 반사된 각도와 가까울 수록 매우 밝은 빛을 보게 되지만, 특정 반경을 넘어서게 될 경우 반사된 빛 자체를 볼 수 없게 될 수 도 있다.


퐁 셰이딩

앞서 다룬 주변광(Ambient), 분산광(Diffuse), 반영광(Specular)을 사용하면 우리는 입체감 있는 3차원 물체를 다음과 같이 표현할 수 있다.

▲ 퐁 셰이딩(출처: 위키피디아)

주변광 분산광 반영광을 활용하여서 퐁 셰이딩 값을 계산하기 위해서는 다음과 같은 데이터들이 필요하다.

▲ 퐁 셰이딩 계산 다이어그램

  • \(\hat{n}\): 정규화된 표면 법선 벡터
  • \(\hat{l}\): 표면에서 광원으로의 정규화된 벡터
  • \(\hat{v}\): 표면에서 카메라 위치로의 정규화된 벡터
  • \(\hat{r}\): \(\hat{n}\)에 대한 \(-\hat{l}\)의 정규화된 반사 벡터
  • \(\alpha\): 정반사 지수(오브젝트의 광택 정도)

추가적으로 광원에 대한 색상은 다음과 같이 정의한다.

  • \(k_a\): 주변광 색상
  • \(k_a\): 분산광 색상
  • \(k_a\): 반영광 색상

위 데이터들을 사용해서 퐁 반사 모델에서 표면의 광원은 다음과 같이 계산한다.
\[Ambient = k_a\]

\[Diffuse = k_d(\hat{n}\cdot{\hat{l}})\]

\[Specular = k_s(\hat{r}\cdot{\hat{v}})^{\alpha}\]

\[Phong = Ambient + Diffuse + Specular \]

\[= k_a + k_d(\hat{n}\cdot{\hat{l}}) + k_s(\hat{r}\cdot{\hat{v}})^{\alpha}\]

이 때 주의할 점은 \(\hat{n}\cdot{\hat{l}}\)이 \(0\)이 될 때이다. \(\hat{l}\)와 \(\hat{n}\)의 내적이 음수라는 뜻은 이 두 벡터가 이루는 각이 90도가 넘었음을 뜻한다. 즉 표면의 뒤에서 빛을 비추는 상황이다. 따라서 \(\hat{n}\cdot{\hat{l}}\)가 음수라면 \(Diffuse, Specular\)의 값은 항상 0으로 보정해야한다.

퐁 셰이딩 구현

프로젝트에서 퐁셰이딩을 반영하기위해서 수정해야 하는 곳들이 많다. 이에 대해서는 전체 소스코드를 참고하자. 여기에서는 위 퐁셰이딩을 계산이 이루어지는 코드를 살펴본다. 퐁 셰이딩은 각 픽셀마다 필요한 조명값을 계산하는 방법이다. 그래서 프레그먼트 셰이더에서 이를 계산하는 것이 적합하다. 아래는 퐁셰이딩을 구현한 Phong.fragglsl 파일이다.

Phong.frag 구현

#version 330

// 방향광을 위한 구조체 정의
struct DirectionalLight
{
    // 빛의 방향
    vec3 mDirection;
    // 난반사 색상
    vec3 mDiffuseColor;
    // 정반사 색상
    vec3 mSpecColor;
};

//********* C++로 부터 입력 받는 uniform 부분**************
// 조명을 위한 uniform
// 세계 공간에서의 카메라 위치
uniform vec3 uCameraPos;
// 주변광
uniform vec3 uAmbientLight;
// 표면에 대한 정반사 지수
uniform float uSpecPower;
// 방향광(지금은 오직 하나)
uniform DirectionalLight uDirLight;
// 제공된 텍스처 좌표로 색상을 얻기 위해 텍스처 샘플러 uniform
uniform sampler2D uTexture;


//****** 버텍스 셰이더로 부터 입력 받는 부분********
// 텍스처와 매핑되는 텍스처 좌표
in vec2 fragTexCoord;
// 세계 공간에서의 법선
in vec3 fragNormal;
// 세계 공간에서의 위치
in vec3 fragWorldPos;


// ******* 프래그먼트 셰이더 출력 부분****************
out vec4 outColor;

void main()
{
    // 표면 법선. 각 버텍스에서는 정규화된 법선벡터를 가지고 있더라도
    // 삼각형 표면의 법선벡터는 버텍스의 법선 벡터를 보간하여서 사용한다.
    // 이때 보간된 값은 정규화된 벡터를 보장하지 않기 때문에 사용하기 전에 정규화 단계를 거쳐야 한다. 
    vec3 N = normalize(fragNormal);

    // 표면에서 광원의로의 벡터
    // 방향광은 한 방향으로 발산하는 특징이 있다.
    // 때문에 표면에서 광원으로 향하는 벡터는 광원의 방향 벡터를 반전시키면 된다.
    vec3 L = normalize(-uDirLight.mDirection);

    // 표면에서 카메라로 향하는 벡터
    vec3 V = normalize(uCameraPos - fragWorldPos);

    // N에 대한 -L의 반사
    // GLSL에서는 반사벡터를 구할 수 있는 함수 reflect()를 제공해준다.
    vec3 R = normalize(reflect(-L, N));

    // 퐁 반사 계산
    vec3 Phong = uAmbientLight;
    float NdotL = dot(N, L);
    if(NdotL > 0)
    {
        vec3 Diffuse = uDirLight.mDiffuseColor * NdotL;
        vec3 Specular = uDirLight.mSpecColor * 
                        pow(max(0.0, dot(R, V)), uSpecPower);
        Phong += Diffuse + Specular;
    }

    // 최종 색은 텍서처 색상 곱하기 퐁 광원 (알파값 = 1)
    outColor = texture(uTexture, fragTexCoord) * vec4(Phong,1.0f);
}

구현 결과

 


출처:

에이콘 출판 <Game Programming in C++> 

한빛미디어 출판 <DirectX 11을 이용한 3D 게임 프로그래밍 입문>

위키피디아