Skip to content

여러 개의 조명

이전 장에서는 OpenGL 조명에 대해 많은 것을 배웠습니다. 퐁 셰이딩, 재질, 라이팅 맵, 다양한 광원 유형에 대해 알아보았죠. 이번 장에서는 지금까지 배운 모든 지식을 활용하여 6개의 활성 광원을 사용하는 조명이 완비된 장면을 만들어 보겠습니다. 태양과 같은 빛을 방향성 광원으로 구현하고, 장면 전체에 4개의 점광원을 배치한 다음, 손전등도 추가할 것입니다.

장면에 여러 광원을 사용하려면 조명 계산을 GLSL 함수로 캡슐화해야 합니다. 여러 광원 유형 각각에 대해 서로 다른 계산이 필요하기 때문에 조명 계산을 할 때 코드가 매우 복잡해지기 때문입니다. 만약 이러한 모든 계산을 메인 함수에서만 수행한다면 코드를 이해하기 어려워질 것입니다.

GLSL에서 함수는 C 언어의 함수와 매우 유사합니다. 함수 이름과 반환 타입이 있으며, main 함수 이전에 함수가 선언되지 않았다면 코드 파일 맨 위에 함수 프로토타입을 선언해야 합니다. 우리는 방향광, 점광, 스포트라이트 등 각 조명 유형별로 별도의 함수를 만들 것입니다.

장면에 여러 개의 광원을 사용할 때 일반적인 접근 방식은 다음과 같습니다. 먼저 프래그먼트의 출력 색상을 나타내는 단일 색상 벡터를 사용합니다. 각 광원에 대해 해당 광원이 프래그먼트에 미치는 영향을 이 출력 색상 벡터에 더합니다. 따라서 장면의 각 광원은 개별적인 영향을 계산하고 최종 출력 색상에 반영합니다. 일반적인 구조는 다음과 같습니다.

out vec4 FragColor;

void main()
{
  // 출력 색상 값을 정의합니다.
  vec3 output = vec3(0.0);
  // 출력에 방향광의 영향력(기여도)를 더합니다.
  output += someFunctionToCalculateDirectionalLight();
  // 모든 포인트 라이트에 대해 동일한 작업을 수행하십시오.
  for(int i = 0; i < nr_of_point_lights; i++)
    output += someFunctionToCalculatePointLight();
  // 그리고 스포트라이트 같은 다른 조명도 추가하세요.
  output += someFunctionToCalculateSpotLight();

  FragColor = vec4(output, 1.0);
}  

실제 코드는 구현 방식에 따라 다를 수 있지만, 일반적인 구조는 동일합니다. 우리는 각 광원의 영향을 계산하고 그 결과 색상을 출력 색상 벡터에 추가하는 여러 함수를 정의합니다. 예를 들어 두 개의 광원이 프래그먼트 가까이에 있다면, 두 광원의 합산된 영향으로 인해 단일 광원만 있을 때보다 프래그먼트가 더 밝게 조명됩니다.

방향광

프래그먼트 셰이더에서 방향광이 해당 프래그먼트에 미치는 영향을 계산하는 함수를 정의하려고 합니다. 이 함수는 몇 가지 매개변수를 받아 계산된 방향광 색상을 반환합니다.

먼저 방향성 광원에 최소한으로 필요한 변수들을 설정해야 합니다. 이 변수들을 DirLight라는 구조체에 저장하고 유니폼 변수로 정의할 수 있습니다. 구조체의 변수들은 이전 장에서 다룬 내용과 유사할 것입니다.

struct DirLight {
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
uniform DirLight dirLight;

그런 다음 dirLight 유니폼을 다음과 같은 프로토타입을 가진 함수에 전달할 수 있습니다.

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);  

C와 C++처럼, 함수를 호출하려면 (이 경우에는 main 함수 내부에서) 함수는 호출하는 함수의 줄 번호보다 앞에 정의되어 있어야 합니다. 하지만 이 경우에는 main 함수 아래에 함수를 정의하는 것이 더 바람직하므로 이 요구 사항이 적용되지 않습니다. 따라서 C에서처럼 main 함수 위에 함수의 프로토타입을 선언합니다.

이 함수는 DirLight 구조체와 계산에 필요한 두 개의 벡터를 필요로 한다는 것을 알 수 있습니다. 이전 장을 성공적으로 완료했다면 이 함수의 내용은 전혀 놀랍지 않을 것입니다.

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // 확산 셰이딩
    float diff = max(dot(normal, lightDir), 0.0);
    // 반사 셰이딩
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 모두 합친 결과
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    return (ambient + diffuse + specular);
} 

기본적으로 이전 장의 코드를 복사하고 함수 인수로 제공된 벡터를 사용하여 방향광의 기여 벡터를 계산했습니다. 그 결과, 주변광, 확산광 및 반사광의 기여도가 단일 색상 벡터로 반환됩니다.

점광원

방향광과 마찬가지로, 주어진 프래그먼트에 대한 점광의 기여도(감쇠 포함)를 계산하는 함수를 정의하려고 합니다. 방향광과 마찬가지로, 점광에 필요한 모든 변수를 지정하는 구조체를 정의하고자 합니다.

struct PointLight {    
    vec3 position;

    float constant;
    float linear;
    float quadratic;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
#define NR_POINT_LIGHTS 4  
uniform PointLight pointLights[NR_POINT_LIGHTS];

보시다시피, GLSL에서 전처리 지시문을 사용하여 장면에 필요한 포인트 라이트의 개수를 정의했습니다. 그런 다음 이 NR_POINT_LIGHTS 상수를 사용하여 PointLight 구조체 배열을 생성합니다. GLSL의 배열은 C 언어의 배열과 유사하며 두 개의 대괄호를 사용하여 생성할 수 있습니다. 현재 데이터를 채워 넣을 PointLight 구조체가 4개 있습니다.

점광원의 함수 프로토타입은 다음과 같습니다.

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);  

이 함수는 필요한 모든 데이터를 인수로 받아 특정 포인트 라이트가 프래그먼트에 미치는 색상 기여도를 나타내는 vec3를 반환합니다. 다시 한번, 적절한 복사 붙여넣기를 통해 다음과 같은 함수를 얻을 수 있습니다.

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // 확산 셰이딩
    float diff = max(dot(normal, lightDir), 0.0);
    // 반사 셰이딩
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // 감쇠
    float distance    = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
                 light.quadratic * (distance * distance));    
    // 모두 합친 결과
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    ambient  *= attenuation;
    diffuse  *= attenuation;
    specular *= attenuation;
    return (ambient + diffuse + specular);
} 

이러한 기능을 함수로 추상화하면 코드 중복 없이 여러 개의 점광원에 대한 조명을 쉽게 계산할 수 있다는 장점이 있습니다. 메인 함수에서는 점광원 배열을 순회하면서 각 점광원에 대해 CalcPointLight 함수를 호출하는 루프를 생성하기만 하면 됩니다.

모든 것을 종합해 보면

이제 방향광을 위한 함수와 점광원을 위한 함수를 모두 정의했으므로 이 모든 것을 메인 함수에 통합할 수 있습니다.

void main()
{
    // properties
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    // phase 1: Directional lighting
    vec3 result = CalcDirLight(dirLight, norm, viewDir);
    // phase 2: Point lights
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);    
    // phase 3: Spot light
    //result += CalcSpotLight(spotLight, norm, FragPos, viewDir);    

    FragColor = vec4(result, 1.0);
}

각 광원 유형은 모든 광원이 처리될 때까지 최종 출력 색상에 영향을 미칩니다. 최종 색상은 장면 내 모든 광원의 색상 영향을 종합적으로 반영합니다. CalcSpotLight 함수에 대한 자세한 설명은 독자 여러분의 몫으로 남겨두겠습니다.

이 접근 방식에는 광원 유형 함수 전반에 걸쳐 중복 계산이 많이 포함되어 있습니다(예: 반사 벡터, 확산 및 반사광 항 계산, 재질 텍스처 샘플링). 따라서 최적화할 여지가 있습니다.

방향광 구조체의 유니폼 값을 설정하는 것은 그다지 어렵지 않겠지만, 점광원의 유니폼 값은 PointLight 구조체의 배열이기 때문에 어떻게 설정해야 할지 궁금할 수 있습니다. 이는 이전에 다룬 적이 없는 내용입니다.

다행히도 그다지 복잡하지는 않습니다. 구조체 배열의 유니폼 값을 설정하는 방법은 단일 구조체의 유니폼 값을 설정하는 방법과 동일하지만, 이번에는 유니폼의 위치를 ​​조회할 때 적절한 인덱스를 정의해야 합니다.

lightingShader.setFloat("pointLights[0].constant", 1.0f);

여기서는 pointLights 배열에서 첫 번째 PointLight 구조체를 인덱싱하고 내부적으로 해당 상수 변수의 위치를 ​​가져온 다음 이 값을 1.0으로 설정합니다.

4개의 포인트 라이트 각각에 대한 위치 벡터도 정의해야 한다는 점을 잊지 맙시다. 따라서 장면 전체에 포인트 라이트를 좀 더 넓게 배치해 보겠습니다. 포인트 라이트의 위치를 ​​담을 glm::vec3 배열을 하나 더 정의하겠습니다.

glm::vec3 pointLightPositions[] = {
    glm::vec3( 0.7f,  0.2f,  2.0f),
    glm::vec3( 2.3f, -3.3f, -4.0f),
    glm::vec3(-4.0f,  2.0f, -12.0f),
    glm::vec3( 0.0f,  0.0f, -3.0f)
};  

다음으로 pointLights 배열에서 해당 PointLight 구조체를 인덱싱하고, 해당 구조체의 position 속성을 방금 정의한 위치 중 하나로 설정합니다. 또한 이제 광원 큐브를 하나만 그리는 대신 4개를 그려야 합니다. 컨테이너에서 했던 것처럼 각 광원 객체에 대해 서로 다른 모델 행렬을 생성하면 됩니다.

만약 손전등까지 사용한다면, 모든 빛이 합쳐진 결과는 대략 다음과 같습니다.

보시다시피 하늘 어딘가에 태양과 같은 형태의 전역 광원이 있는 것 같고, 장면 전체에 4개의 조명이 흩어져 있으며, 플레이어의 시점에서는 손전등이 보입니다. 꽤 멋지지 않나요?

최종 애플리케이션의 전체 소스 코드는 여기에서 확인할 수 있습니다.

이 이미지는 이전 장에서 사용했던 기본 조명 속성으로 설정된 모든 광원을 보여줍니다. 하지만 이러한 값을 조정해 보면 상당히 흥미로운 결과를 얻을 수 있습니다. 아티스트와 레벨 디자이너는 일반적으로 대형 에디터에서 이러한 모든 조명 변수를 조정하여 조명이 환경과 잘 어울리도록 합니다. 이 간단한 환경에서도 조명 속성을 조정하는 것만으로도 꽤 흥미로운 시각 효과를 만들어낼 수 있습니다.

조명을 더 잘 반영하도록 배경 색상도 변경했습니다. 보시다시피 조명 매개변수를 간단히 조정하는 것만으로도 완전히 다른 분위기를 연출할 수 있습니다.

이제 OpenGL 조명에 대해 꽤 잘 이해하셨을 겁니다. 지금까지 배운 내용을 바탕으로 흥미롭고 시각적으로 풍부한 환경과 분위기를 만들어낼 수 있습니다. 다양한 값을 조절해 보면서 자신만의 분위기를 만들어 보세요.

연습 문제

  • 마지막 이미지에서처럼 조명의 속성 값을 조정해서 다양한 분위기를 (어느 정도) 재현할 수 있을까요? (해결 방법)

Comments