OpenGL

[Learn OpenGL 번역] 3-6. 조명 - Multiple lights

짱승_ 2018. 7. 17. 21:31

Multiple lights

조명/Multiple-lights

  이전 튜토리얼에서 OpenGL의 lighting에 대한 꽤 많은 것들을 배웠습니다. Phong shading, materials, lighting maps에 대하여 배웠고 light caster의 여러가지 유형에 대해서도 배웠습니다. 이번 강좌에서는 지금까지 배웠던 지식들을 사용하여 6개의 활성화된 광원들로 완전히 비춰진 scene을 생성할 것입니다. directional light로 태양과 같은 빛을 시뮬레이션하고 4개의 point light를 사용하여 scene 전체에 빛을 산란하고 flashlight도 추가할 것입니다.


  하나 이상의 광원을 scene에 사용하기 위해 lighting 계산을 GLSL의 functions에 캡슐화해야합니다. 이렇게 하는 이유는 여러가지 빛을 요구된 다른 계산법들로 lighting을 계산하는 것은 코드를 끔찍하게 만들기 때문입니다. 만약 우리가 이 모든 계산들을 main 함수 안에서 수행했다면 코드는 이해하기 어려운 형태가 됐을 것입니다.


  GLSL의 Functions는 C에서의 함수들과 비슷합니다. 함수의 이름과 리턴 타입을 가지고있고 main 함수 전에 선언되어 있지 않다면 코드 파일의 최상위에 포로토타입을 선언해야 합니다. 각 light의 타입에 대한 함수를 따로 만들 것입니다(directional lights, point lights, spotlights).


  여러가지 light를 사용할 때 접근방식은 일반적으로 다음과 같이 합니다: 우리는 fragment의 출력 컬러를 나타내는 하나의 컬러 벡터를 가집니다. 각 light들을 위해 이 fragment에 light가 기여하는 컬러를 이 fragment의 출력 컬러에 더합니다. 그리고 scene의 각 light는 앞서 언급한 fragment에 미치는 효과를 계산하고 최종 출력 컬러에 기여하게 될 것입니다. 일반적인 구조는 다음과 같습니다.


out vec4 FragColor;
  
void main()
{
  // 출력 컬러 값을 정의
  vec3 output = vec3(0.0);
  // Directional light의 기여를 출력에 더합니다.
  output += someFunctionToCalculateDirectionalLight();
  // 모든 point light들에도 동일하게 수행합니다.
  for(int i = 0; i < nr_of_point_lights; i++)
  	output += someFunctionToCalculatePointLight();
  // 다른 light들도 더합니다(spotlight 같은 것들)
  output += someFunctionToCalculateSpotLight();
  
  FragColor = vec4(output, 1.0);
}  

  실제 코드는 구현에 따라 다를 수도 있을 것입니다. 하지만 일반적인 구조는 동일합니다. 광원에 대한 효과를 계산하는 여러가지 함수들을 정의하고 그 결과 컬러를 출력 컬러 벡터에 더합니다. 예를 들어 2개의 광원이 fragment와 가까이에 있다면 그들이 혼합된 기여도는 하나의 광원이 fragment를 비추는 것보다 좀 더 밝게 비춥니다.

Directional light

  우리가 해야할 일은 fragment shader에 함수를 정의하는 것입니다. 이 함수는 해당 fragment에 대한 directional light의 기여도를 계산합니다. 몇 가지의 파라미터를 받고 계산된 directional lighting 컬러를 리턴합니다.


  먼저 우리는 directional 광원에 최소한으로 필요한 변수들을 설정해야합니다. DirLight라고 불리는 struct에 이 변수들을 저장할 수 있고 이는 uniform으로 선언됩니다. 필요한 변수들은 이전 튜토리얼과 유사합니다.


struct DirLight {
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
uniform DirLight dirLight;

  그런 다음 dirLight uniform을 다음과 같이 함수에 넘겨줍니다.


vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);  
  C언어와 C++와 마찬가지로 함수를 호출하기 위해서는(이 경우에 main 함수 안에서) 이 함수는 호출하는 라인 전의 어딘가에 정의되어야 합니다. 이 경우에 우리는 이 함수를 main 함수 밑에 정의하려고 하므로 이 요구사항을 만족하지 않습니다. 그러므로 C언어에서와 마찬가지로 이 함수의 포로토타입을 main 함수 위의 어딘가에 선언해놓아야 합니다.

  이 함수는 DirLight struct와 2개의 다른 벡터들을 필요로 하고 있다는 것을 볼 수 있습니다. 여러분이 이전 강좌를 완벽하게 마치셨다면 이 함수의 내용은 알 수 있을 것입니다.


vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // specular shading
    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);
}  

  우리는 기본적으로 이전의 강좌에서 코드를 복사했고 함수의 파라미터로 주어진 벡터들을 directional light의 기여 벡터를 계산하는 데에 사용했습니다. 결과 ambient, diffuse, specular 기여도는 하나의 컬러 벡터로 리턴됩니다.

Point light

  Directional light와 마찬가지로 주어진 fragment에 대한 point light가 가지는 기여도를 계산할 함수를 정의해야 합니다. attenuation과 함께 말이죠. directional light와 마찬가지로 point light에 필요한 모든 변수들을 지정하는 struct를 정의해야합니다.


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];

  우리가 scene에 배치할 point light의 갯수를 GLSL에서 전처리기로 선언한 것을 볼 수 있습니다. 그런 다음 우리는 이 NR_POINT_LIGHTS 상수를 PointLight struct의 배열을 생성하는 데에 사용합니다. GLSL에서의 배열은 C 배열과 비슷합니다. 2개의 대괄호를 사용하여 배열을 생성할 수 있습니다. 지금 당장 우리는 4개의 PointLight struct를 가지고 있습니다.

  또한 우리는 간단히 모든 다른 유형의 light들에 대해 필수적인 변수들을 모두 포함하는 하나의 큰 struct(light 유형마다 다른 struct를 가지는 것 대신)를 정의하고 이 struct를 각 함수에서 사용하며 필요없는 변수들을 간단히 무시할 수도 있습니다. 하지만 저는 개인적으로 현재 방법이 좀 더 직관적이고 코드에 추가적인 라인을 줄일 수 있어 메모리 일부를 절약할 수 있다고 생각합니다.

  Point light 함수의 프로토타입은 다음과 같습니다.


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

  이 함수는 필요한 모든 데이터를 파라미터로 받고 fragment에 대한 특정한 point light의 기여 컬러를 나타내는 vec3를 리턴합니다. 다시 한번, 복사 붙여넣기를 사용하여 다음과 같은 함수를 생성합니다.


vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // attenuation
    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);
} 

  이 기능을 이 처럼 함수에 추상화하는 것은 끔찍한 중복 코드 없이 여러개의 point light들을 쉽게 계산할 수 있다는 장점을 가집니다. main 함수에서 우리는 간단히 각각의 point light에 대해 CalcPointLight 함수를 호출하도록 반복문을 만들 수 있습니다.

모두 함께

  이제 우리는 directional light와 point light 함수를 정의하였고 이들을 main 함수에 모두 같이 넣을 수 있습니다.


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

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

  모든 광원이 처리될때까지 각 light 타입은 그들의 기여도를 최종 출력 컬러에 더합니다. 최종 컬러는 scene에 결합된 모든 광원들의 컬러 효과를 포함하고 있습니다. 원한다면 spotlight도 구현하여 출력 컬러에 효과를 추가할 수도 있습니다. CalcSpotLight 함수는 독자들의 연습을 위해 남겨두도록 하겠습니다.


  Directional light를 위한 unform을 설정하는 것은 많이 다르지 않습니다. 하지만 point light들의 uniform 값을 어떻게 설정해야하는지 궁금할 것입니다. point light uniform은 이제 PointLight struct의 배열이기 때문입니다. 이는 전에 다루었던 것이 아닙니다.


  다행히도 이는 그렇게 복잡하지 않습니다. struct의 배열에 대한 uniform을 설정하는 것은 하나의 struct uniform을 설정하는 것과 비슷합니다. 이번에는 uniform의 location에 대한 적절한 인덱스를 정의해야 한다는 것만 다를뿐입니다.


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

  여기에서 pointLights 배열의 첫 번째 PointLight struct를 인덱싱하여 constant 변수의 location을 얻고 있습니다. 이는 불행하게도 4 개의 각각의 light에 대한 모든 uniform 일일히 설정해주어야 한다는 것을 의미합니다. 이는 28번의 uniform 호출을 발생시킵니다. 이를 피하기 위해 추상화를 시도해볼 수 있습니다. uniform을 설정하는 point light 클래스를 정의함으로써 말이죠. 하지만 결국에는 이 방법에서도 모든 light들의 uniform을 설정해야합니다.


  또한 우리는 point light들의 위치 벡터를 정의해야 scene 주위에 그들을 배치할 수 있다는 것을 잊지마세요. pointlight의 위치들을 가지고 있는 또다른 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 struct를 인덱싱하여 position 속성을 우리가 방금 정의한 위치들 중 하나로 설정합니다. 또한 하나가 아닌 4개의 light 큐브를 그려야합니다. 컨테이너들을 여러개 그린 것과 마찬가지로 간단히 각 light 오브젝트에 대한 서로 다른 model 행렬을 생성합니다.


  여러분이 flashlight를 사용한다면 모든 light의 결합은 다음과 같이 보일 것입니다.



  보시다시피 하늘 어딘가에 전체적인 light(태양과 비슷한)의 형태가 나타난 것을 볼 수 있습니다. scene에 빛을 산란하는 4개의 light들을 가지고 있고 flashlight는 플레이어의 시점을 기준으로 보이게 됩니다. 꽤 깔끔해보이지 않나요?


  최종 응용 프로그램의 전체 소스 코드는 여기에서 확인할 수 있습니다.


  이 이미지는 이전의 강좌들에서 다루었던 기본적인 light 속성들을 가지는 광원들을 보여줍니다. 하지만 이 값들을 다양하게 시도해본다면 여러분은 꽤 흥미로운 결과를 얻을 수 있습니다. 아티스트와 레벨 에디터들은 일반적으로 이러한 모든 lighting 변수들을 가지고 놉니다. lighting이 환경에 어울리도록 만들기 위해서 말이죠. 여러분이 만든 간단한 빛을 가진 환경을 사용하여 좀 흥미로운 비주얼을 생성할 수 있습니다. 간단히 light들의 속성들을 수정함으로써 말이죠.



  또한 우리는 빛을 더 잘 반사하도록 하기 위해 clear 컬러를 수정할 수도 있습니다. 단순히 lighting 파라미터의 일부를 조정하면 완전히 다른 분위기를 만들 수 있음을 알 수 있습니다.


  이제 여러분은 OpenGL의 lighting에 대해 꽤 잘 이해할 수 있게 되었습니다. 지금까지의 지식들을 통해 우리는 이미 흥미롭고 시각적으로 풍부한 환경과 분위기를 생성할 수 있습니다. 여러가지 변수들을 가지고 여러분만의 분위기를 만들어보세요.

연습

  • light의 속성 값들을 수정하여 마지막 이미지의 여러 분위기들을 만들 수 있나요? 해답



출처 : https://learnopengl.com, Joey de Vries

반응형