Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 3-5. 조명 - Light casters 본문

OpenGL

[Learn OpenGL 번역] 3-5. 조명 - Light casters

짱승_ 2018. 7. 16. 09:05

Light casters

조명/Light-casters

  지금까지 우리가 사용한 모든 lighting은 공간에 하나의 점으로 나타나는 하나의 광원으로 인한 것이었습니다. 이는 멋진 결과를 보여주지만 현실에서는 여러가지 유형의 빛들이 존재합니다. 오브젝트에 빛을 cast(발하다)하는 광원을 light caster라고 합니다. 이번 강좌에서는 여러 유형의 light caster들을 다룰 것입니다. 다른 광원을 시뮬레이션하는 법을 배우는 것은 여러분의 환경의 질을 더욱 높게 만들 툴박스에 들어갈 또 하나의 도구입니다.


  먼저 directional light(방향이 있는 빛)에 대해서 다룰 것이고 그 다음엔 point light, 마지막으로는 spotlight를 다룰 것입니다. 다음 강좌에서는 이들을 하나의 scene에 조합해볼 것입니다.

Directional Light

  광원이 멀리 있을때 광원으로부터 오는 광선은 서로서로에 대해 거의 평행합니다. 마치 모든 광선들이 동일한 방향으로부터 오는 것처럼 보입니다. 오브젝트의 위치나 viewer의 위치에 상관없이 말이죠. 광원이 무한히 멀리 있을 때 모든 광선들이 동일한 방향(이는 광원의 위치에 독립적입니다)을 가지기 때문이 이 광원을 directional light라고 부릅니다.


  directional light의 좋은 예는 우리가 알고있는 태양입니다. 태양은 우리와 무한정으로 멀리 있지는 않지만 lighting 계산에서 무한정으로 멀리 있다고 간주할 정도로 우리와 멀리 떨어져 있습니다. 태양에서 오는 모든 광선들은 다음 이미지에서 보는 것과 같이 서로 평행합니다.



  모든 광선들이 평행하기 때문에 각 오브젝트들이 광원의 위치와 어떠한 관계가 있는지에 대해서는 상관없습니다. 빛의 방향은 scene에 존재하는 각각의 오브젝트에 모두 동일하기 때문입니다. 빛의 방향 벡터가 동일하게 유지되기 때문에 lighting 계산이 scene에 존재하는 각 오브젝트들에게 비슷하게 적용됩니다.


  빛에 대한 위치 벡터 대신에 방향 벡터를 정의하면 이러한 directional light를 만들 수 있습니다. shader 계산은 대부분 동일하지만 이번에는 빛의 position 벡터를 사용하여 계산된 lightDir 벡터 대신에 빛의 direction 벡터를 직접적으로 사용할 것입니다.


struct Light {
    // vec3 position; // Directional light를 사용할 때는 더 이상 필요하지 않습니다.
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
  ...
}

  먼저 light.direction 벡터의 부호를 바꾸었다는 것을 알아두세요. 지금까지 우리가 사용해왔던 lighting 계산들은 fragment로 부터 광원으로 향하는 빛의 방향을 원합니다. 하지만 사람들은 일반적으로 directional light를 광원으로부터 fragment로 향하는 방향으로 나타내는 것을 선호합니다. 그러므로 우리는 빛의 방향 벡터의 부호를 바꾸어 반대 방향을 가지도록 해야합니다. 이제 이 방향 벡터는 광원을 향하고 있습니다. 또한 입력받은 벡터가 단위 벡터라고 생각하는 것은 어리석은 생각이므로 이 벡터를 확실히 정규화합니다.


  최종 lightDir 벡터는 diffuse, specular 계산에 사용됩니다.

  Directional light가 모든 여러 오브젝트들에 동일한 효과를 가진다는 것을 명확히 보여주기 위해 우리는 좌표 시스템 강좌에서 사용했던 컨테이너 파티 scene을 재사용할 것입니다. 우리는 먼저 10개의 다른 컨테이너 위치를 정의했었고 적절한 local-to-world 변환을 가지고 있는 각자 다른 model 행렬을 생성했었습니다.


for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model;
    model = glm::translate(model, cubePositions[i]);
    float angle = 20.0f * i;
    model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
    lightingShader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 36);
}

  또한 광원의 방향을 실제로 지정한다는 것을 잊지마세요(지금 우리는 방향을 광원으로부터의 방향으로서 정의합니다; 이 빛의 방향은 아래쪽으로 향한다는 것을 쉽게 알 수 있습니다).


lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f); 	    

  우리는 한동안 빛의 위치 벡터와 방향 벡터를 vec3 타입으로 전달해주었습니다. 하지만 일부 사람들은 모든 벡터들을 vec4 타입으로 정의하려는 경향이 있습니다. 위치 벡터를 vec4 타입의 벡터로 정의할 때 w 요소를 1.0으로 설정하여 이동과 projection이 올바르게 적용되도록 하는 것이 중요합니다. 하지만 방향 벡터를 vec4 타입의 벡터로 정의할 때는 이동 변환이 영향을 미치지 않아야 하기 때문에(이들은 방향만 나타낼 뿐이기 때문) w 요소를 0.0으로 정의합니다.


  방향 벡터들은 vec4(0.2f, 1.0f, 0.3f, 0.0f)와 같이 표현됩니다. 이는 빛의 유형을 쉽게 판단할 수 있는 함수역할도 합니다. w 요소가 1.0이라면 빛의 위치 벡터이고 w 요소가 0.0이라면 빛의 방향벡터인 것입니다. 그래서 이를 연산에서 다음과 같이 사용할 수 있습니다.


if(lightVector.w == 0.0) // note: 부동소수점 오류를 조심하세요.
  // Directional light 계산 수행
else if(lightVector.w == 1.0)
  // 빛의 위치를 사용하는 light 계산 수행

  재밌는 사실: 이는 실제로 광원이 directional light인지 positional light인지 판단하기 위해 구버전의 OpenGL에서 사용되었던 적이 있습니다.

  이제 응용 프로그램을 컴파일하고 scene을 날아다녀 보면 모든 오브젝트에 빛을 발하는 태양 같은 것이 존재하는 것처럼 보일 것입니다. duffuse, specular 컴포넌트들 모두 하늘 어딘가에 광원이 있는 것처럼 동작하는 것을 볼 수 있습니까? 이는 다음 이미지와 같이 보일 것입니다.



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

Point lights

  Directional light들은 전체 scene을 밝히는 전반적인 빛에 사용되는 것이 좋습니다. 하지만 일반적으로 scene 전체에 산란되는 여러 point light들도 사용합니다. point light는 world의 어딘가에 주어진 위치를 갖는 광원입니다. 모든 방향으로 빛을 밝히고 거리에따라 광선은 점점 희미해집니다. 전구나 횃불을 생각하시면 됩니다.



  이전의 강좌들에서 우리는 내내 (간단한) point light를 가지고 작업해왔습니다. 우리는 주어진 빛의 위치로부터의 모든 방향으로 빛을 산란하는 광원을 가지고 있습니다. 하지만 우리가 정의한 광원은 점점 희미해지지 않는 광선을 시뮬레이션한 것이므로 마치 광원이 매우 센 것처럼 보이는 것입니다. 대부분의 3D 시뮬레이션들에서 scene의 전체를 밝히는 것이 아니라 광원과 가까이에 있는 특정 영역만 밝히는 광원을 시뮬레이션합니다.


  이전 강좌의 lighting scene에 10개의 컨테이너들을 추가했었다면 램프 바로 앞의 컨테이너와 멀리 떨어진 컨테이너가 동일한 세기로 빛의 영향을 받았던 것을 알 수 있을 것입니다. 거리에 따라 빛이 약해지는 것에 대한 공식을 사용하지 않았었습니다. 우리는 광원과 가까이 있는 컨테이너들과 비교해서 멀리 떨어진 컨테이너는 약간의 빛만 받기를 원합니다.

Attenuation(감쇠)

  광선이 지나가는 거리에 따라 빛의 세기를 줄이는 것을 일반적으로 attenuation(감쇠)라고 부릅니다. 거리에 따라 빛의 세기를 줄이는 방법 중 하나는 간단히 1차 방정식을 사용하는 것입니다. 이러한 방정식은 빛의 세기를 연속적으로 줄일 수 있으므로 멀리 있는 오브젝트는 적은 빛을 받을 수 있게 만들 수 있습니다. 하지만 이러한 1차 함수는 속임수처럼 보여질 수 있습니다. 현실에서 빛들은 바로 옆에 있으면 아주 밝지만 시작지점에서 광원의 밝기는 아주 빠르게 감소되고 남아있는 빛의 세기는 점점 더 느리게 감소됩니다. 따라서 빛의 밝기를 줄이는 데에 다른 공식이 필요합니다.


  운좋게도 일부 똑똑한 사람들이 이미 우리를 위해 이 것을 알아냈습니다. 다음 공식은 광원과 fragment 사이의 거리를 기반으로 하는 attenuation 값을 계산합니다. 우리는 나중에 이 값을 빛의 세기 벡터에 곱할 것입니다.

(1)Fatt=1.0Kc+Kld+Kqd2

  여기에서 d 는 fragment에서 광원까지의 거리를 나타냅니다. 그런 다음 attenuation 값을 계산하기 위해 3가지의 (설정가능한) 항(constant(상수) 항인 K c , linear(1차) 항인 K l , quadratic(2차) 항인 K q )들을 정의합니다.


  • 이 상수항은 일반적으로 1.0을 유지합니다. 최종 결과의 분모를 1보다 작게 만들지 않도록 하기위해 존재합니다. 그렇지 않으면 특정 거리에서 빛의 세기를 증폭시켜 원하는 효과를 낼 수 없기 때문입니다.
  • 이 1차항은 거리 값과 곱해지며 1차원 방법으로 세기를 감소시킵니다.
  • 이 2차항은 거리의 사분면과 곱해지고 2차원 적으로 광원의 세기를 감소시킵니다. 거리가 가까울 때 이 2차항은 1차항에 비해 덜 중요해질 것이고 거리가 멀때는 1차항보다 중요해질 것입니다.

  이 2차항때문에 2차항이 1차항을 능가할정도로 거리가 충분히 커질 때까지 빛의 세기는 주로 1차원적인 방법으로 감소되어 빠르게 감소될 것입니다. 최종적인 효과는 빛이 가까운 범위내에 있을 때 상당히 밝고 거리에따라 빠르게 어두워지며 결국에는 점점 느린 속도로 어두워지게 되는 효과입니다. 다음 그래프는 100 크기의 거리에서 이러한 attenuation이 가지는 효과를 보여줍니다.



  이 빛은 거리가 작을 때 높은 세기를 가지고 거리가 커질수록 세기가 상당히 많이 줄어들고 느리게 0으로 다가갑니다. 이 것이 정확히 우리가 원하는 것입니다.

올바른 값을 선택

  하지만 이 3개의 항에 어떠한 값을 설정해야할까요? 올바른 값을 설정하는 것은 많은 요소에 따라 다릅니다. 환경, 빛이 영향을 미칠 거리, 빛의 유형 등이 있습니다. 대부분의 경우에 적당한 정도로 조정하는 것은 단순히 경험의 문제입니다. 다음 표는 특정한 반지름(거리)를 커버하는 일종의 현실적인 광원을 시뮬레이션하기 위해 가질 수 있는 이 항들의 값을 보여줍니다. 첫 번째 열은 주어진 항들을 사용하여 빛이 커버할 수 있는 거리를 지정합니다. 이 값들은 대부분의 빛에서의 좋은 시작점이 될 수 있습니다. Ogre3D's wiki의 도움을 받아서 말이죠.


Distance Constant Linear Quadratic
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

  보시다시피 상수항 K c 는 모든 경우에서 1.0을 유지합니다. 1차항 K l 는 일반적으로 큰 거리를 커버하기 위해서는 아주 작은 값을 가지고 2차항 K q 도 작아집니다. 이 값들로 실험을 하여 여러분의 구현에 어떠한 효과를 내는지 살펴보세요. 우리의 환경에서 거리는 32 ~ 100이 일반적으로 대부분의 빛에 대해 충분합니다.

Attenuation 구현

  Attenuation을 구현하기 위해 fragment에 추가적인 3개의 값이 필요합니다. 공식에서 constant, linear, quadratic 항들입니다. 이들을 전에 정의했었던 Light struct 안에 저장해놓는 것이 좋습니다. Directional Light 섹션에서 했던 것이 아닌 이 전의 강좌에서 했던 것처럼 lightDir을 계산합니다.


struct Light {
    vec3 position;  
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
	
    float constant;
    float linear;
    float quadratic;
}; 

  그런 다음 OpenGL에서 항들을 설정합니다. 우리는 빛이 50 만큼의 거리를 커버하기를 원하므로 표의 적절한 constant, linear, quadratic 항들을 사용할 것입니다.

		
lightingShader.setFloat("light.constant",  1.0f);
lightingShader.setFloat("light.linear",    0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);	    

  Fragment shader에서 attenuation을 구현하는 것은 비교적 간단합니다. 공식을 기반으로 간단히 attenuation 값을 계산하고 이 것을 ambient, diffuse, specular 컴포넌트에 곱하기만 해주면 됩니다.


  공식에 필요한 광원까지의 거리가 필요합니다. 벡터의 길이를 계산하는 방법을 기억하나요? fragment와 광원 사이의 거리를 얻음으로써 distance(거리) 항을 얻을 수 있습니다. GLSL의 length함수를 사용하여 벡터의 길이를 얻을 수 있습니다.


float distance    = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance + 
    		    light.quadratic * (distance * distance));    

  그런 다음 이 attenuation 값을 ambient, diffuse, specular 컬러에 곱하여 lighting 계산에 포함시킵니다.

  Ambient 요소를 건드리지 않고 ambient lighting이 거리에 따라 어두워지지 않게 할 수도 있습니다. 하지만 만약 한 개 이상의 광원을 사용하게 된다면 모든 ambient 컴포넌트들은 쌓이게 될 것입니다. 이 경우에 ambient lighting에도 attenuation을 적용해야 합니다. 여러분의 환경에서 최적의 조건으로 설정해보세요.

ambient  *= attenuation; 
diffuse  *= attenuation;
specular *= attenuation;   

  응용 프로그램을 실행시키면 다음과 같이 보일 것입니다.



  이제 광원과 가장 가까운 컨테이너가 가장 밝게 비치는 것을 볼 수 있습니다. 멀리 떨어져 있는 컨테이너들은 거의 빛을 받지 못합니다. 여기에서 응용 프로그램의 소스 코드를 확인할 수 있습니다.


  따라서 point light는 lighting 계산에 적용되는 위치와 attenuation을 설정할 수 있는 광원입니다. 우리의 lighting 무기고를 위한 또다른 유형의 light입니다.

Spotlight

  우리가 다룰 마지막 유형의 light는 spotlight입니다. Spotlight는 환경의 어딘가에 위치한 광원입니다. 모든 방향으로 광선을 쏘지 않고 특정 방향으로만 쏩니다. 결과적으로 spotlight 방향의 특정 반지름 내부에 있는 오브젝트만 밝아지고 나머지 모두는 어두워집니다. spotlight의 좋은 예는 가로등과 손전등입니다.


  OpenGL의 spotlight는 world-space에서의 위치, 방향, cutoff 각(spotlight의 반지름을 지정)으로 나타내집니다. 각 fragment에 대해 spotlight의 cutoff 방향 사이(원뿔 내부)에 있는지 계산합니다. 만약 그렇다면 그에 맞춰서 fragment를 밝힙니다. 다음 이미지는 spotlight가 어떻게 동작하는지에 대한 개념을 보여줍니다.



  • LightDir: fragment에서 광원까지의 방향을 나타내는 벡터
  • SpotDir: spotlight가 겨누고 있는 방향
  • Phi ϕ: spotlight의 반지름을 지정하는 cutoff 각입니다. 이 각 외부에 있는 모든 것들은 spotlight에 의한 빛을 받지 못합니다.
  • Theta θ: LightDir 벡터와 SpotDir 벡터 사이의 각도입니다. spotlight의 내부에 있기때문에 θ 값은 Φ 값보다 작아야합니다.

  그래서 기본적으로 우리가 필요한 것은 LightDir 벡터와 SpotDir 벡터를 내적(두 벡터의 내적은 두 벡터 사이의 각에 대한 cosine을 리턴한다는 것을 기억하나요?)하여 이를 cutoff 각 ϕ와 비교하는 것입니다. 이제 여러분은 손전등 모양과 비슷한 것을 만들려고 하는 spotlight에 대해 모든 것을 이해하였습니다.

Flashlight

  Flashlight는 viewer의 위치에 있고 일반적으로 플레이어의 관점을 향해 똑바로 겨누고 있는 spotlight입니다. 기본적으로 flashlight는 일반적인 spotlight이지만 위치와 방향이 플레이어의 위치와 방향에 따라 계속해서 업데이트된다는 점이 다릅니다.


그래서 fragment shader를 위해 필요한 값들은 spotlight의 위치 벡터(빛의 방향을 계산하기 위해), 방향 벡터, cutoff 각입니다. 우리는 이 값들을 Light struct에 저장할 수 있습니다.


struct Light {
    vec3  position;
    vec3  direction;
    float cutOff;
    ...
};    

  그 다음 적절한 값들을 shader에 넘겨줍니다.


lightingShader.setVec3("light.position",  camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff",   glm::cos(glm::radians(12.5f)));

  Cutoff 값을 각도로 설정하지 않고 각에 대한 cosine 값을 계산하고 이 값을 fragment shader로 전달하도록 설정한 것을 볼 수 있습니다. 이에 대한 이유는 fragment shader에서 LightDirSpotDir 벡터의 내적을 계산하고 있는데 내적은 각이 아닌 cosine 값을 반환하므로 cosine 값과 각을 직접적으로 비교할 수 없기 때문입니다. 각을 구하기 위해서는 내적의 결과에 대해 cosine의 역함수를 사용하여야 하는데 이는 비용이 많이 드는 연산입니다. 그래서 약간의 성능 향상을 위해 주어진 cutoff 각에 대한 cosine 값을 계산하고 이 결과를 fragment shader에 전달합니다. 두 각 모두 이제 cosine으로 표현되었기 때문에 비용이 많이 드는 연산을 할 필요없이 직접적으로 비교를 할 수 있습니다.


  이제 남은 것은 theta θ 값을 계산하고 이를 cutoff ϕ 값과 비교하여 spotlight의 내부에 있는지 외부에 있는지 판단하는 것입니다.


float theta = dot(lightDir, normalize(-light.direction));
    
if(theta > light.cutOff) 
{       
  // Lighting 계산 수행
}
else  // 아니면, ambient light를 사용하므로 spotlight 외부에 있더라도 완전히 어둡지는 않습니다.
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

  먼저 lightDir 벡터와 부호를 바꾼 direction 벡터(광원으로부터 나오는 것이 아닌 광원을 향하는 벡터가 필요하기 때문)의 내적을 계산합니다. 관련된 모든 벡터들을 정규화 했는지 확인합니다.

  if문의 조건식에 왜 < 기호를 쓰지 않고 > 기호를 썼는지 궁금할 것입니다. spotlight 내부에 있으면 theta는 빛의 cutoff 값보다 작아야되지 않을까요? 그건 맞습니다. 하지만 각은 cosine 값으로 나타내어졌다는 것을 잊지마세요. 각이 0이면 cosine 값인 1.0으로 나타내어지고 각이 90도면 cosine 값인 0.0으로 나타내어집니다. 이는 다음 이미지에서 볼 수 있습니다.



  여러분은 이제 cosine 값이 1.0에 가까울수록 각도가 작다는 것을 알 수 있을 것입니다. 이제 왜 theta가 cutoff 값보다 커야하는 지 생각해봅시다. cutoff 값은 현재 cosine 12.5로 설정되어 있습니다. 이는 0.9978과 동일하므로 cosine theta 값이 0.99791.0 사이에 있어야 spotlight 내부에 fragment가 존재한다는 사실을 의미합니다.

  응용 프로그램을 실행하면 spotlight의 원뿔 내부에 있는 fragment들에만 빛을 비추는 spotlight를 볼 수 있습니다. 이는 다음과 같이 보일 것입니다.



  여기에서 전체 소스 코드를 확인할 수 있습니다.


  여전히 뭔가 아쉬워 보입니다. spotlight가 명백한 외곽선을 가지고 있기 때문입니다. fragment가 spotlight 원뿔의 외곽선에 닿아있는 곳마다 멋지게 희미해지는 것이 아니라 완전히 닫아버립니다. 현실적인 spotlight는 외곽선에서 점차적으로 빛을 감소시킵니다.

부드러운 외곽선

  부드러운 외곽선을 가진 spotlight를 생성하기 위해 inner(내부) 원뿔과 outer(외부) 원뿔을 가지는 spotlight를 시뮬레이션해야합니다. 우리는 내부 원뿔은 이전에 정의했던 원뿔로 설정하고 내부 원뿔에서 외부 원뿔로 갈수록 점점 빛이 어두워지는 효과를 원합니다.


  외부 원뿔을 생성하기 위해 우리는 간단히 spotlight의 방향 벡터와 외부 원뿔의 벡터(원뿔의 반지름) 사이의 각에 대한 cosine 값을 정의하면됩니다. 그런 다음 fragment가 내부 원뿔과 외부 원뿔 사이에 있으면 빛의 세기 값을 0.0 ~ 1.0 사이로 계산합니다. fragment가 내부 원뿔 안에 존재한다면 빛의 세기는 1.0과 같고 외부 원뿔의 바깥에 존재한다면 0.0과 같습니다.


  다음 공식을 이용하여 이러한 값들을 계산할 수 있습니다.

(2)I=θγϵ

  여기에서 ϵ (epsilon)은 내부 원뿔 (ϕ)과 외부 원뿔 (γ ) 사이의 차입니다 (ϵ=ϕγ ). 최종 결과인 I 값은 현재 fragment의 spotlight 빛의 세기입니다.


  이 공식이 실제로 어떻게 동작하는지 시각화하기 약간 어렵습니다. 그래서 많은 샘플 값들을 알아봅시다.


θ θ 각도 ϕ (내부 cutoff) ϕ 각도 γ (외부 cutoff) γ 각도 ϵ I
0.87 30 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.87 - 0.82 / 0.09 = 0.56
0.9 26 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.9 - 0.82 / 0.09 = 0.89
0.97 14 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.97 - 0.82 / 0.09 = 1.67
0.83 34 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.83 - 0.82 / 0.09 = 0.11
0.64 50 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.64 - 0.82 / 0.09 = -2.0
0.966 15 0.9978 12.5 0.953 17.5 0.9978 - 0.953 = 0.0448 0.966 - 0.953 / 0.0448 = 0.29

  보시다시피 기본적으로 외부 cosine과 내부 cosine 사이를 θ 값을 기반으로 보간하고 있습니다. 여러분이 여전히 정말로 이해하지 못했다면 걱정하지 마세요. 단순히 이 공식을 당연한 것으로 여기고 나중에 여기로 다시 돌아오면 됩니다.


  이제 우리는 빛의 세기 값을 가지고 있지만 이 값은 spotlight 외부에 있으면 음수 값을 가지고 내부 원뿔에 있을 때는 1.0보다 큰 값을 가지게 됩니다. 우리가 이 값을 적절하게 고정시킨다면 fragment shader에 if-else 구문이 필요없어질 것입니다. 그런 다음 계산된 세기 값을 light 컴포넌트들에 곱해줄 수 있습니다.


float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// Ambient에는 영향을 미치지 않으므로 항상 작은 빛을 가지고 있습니다.
diffuse  *= intensity;
specular *= intensity;
...

  첫 번째 파라미터를 0.0 ~ 1.0 사이의 값으로 clamp하는 clamp 함수를 사용한다는 것을 생각하세요. 이 것은 세기 값이 [0, 1] 범위에서 벗어나지 않도록 해줍니다.


  outerCutOff 값을 Light struct에 추가한다는 것을 확인하세요. 그리고 응용 프로그램에서 이것의 unifrom 값을 설정합니다. 다음 이미지에서는 내부 cutoff 각이 12.5, 외부 cutoff 각이 17.5로 설정했습니다.



  아, 이제 좀 났습니다. 내부, 외부 cutoff를 수정하면서 실험해보세요. 여러분의 요구에 맞는 spotlight를 만들어 보세요. 여기에서 전체 소스 코드를 확인할 수 있습니다.


  이러한 flashlight/spotlight 타입의 램프는 공포 게임에 사용하기 완벽하고 directional light와 point light를 조합하면 환하게 만들 수 있습니다. 다음 강좌에서는 지금까지 다루었던 모든 light들과 trick들을 조합해보도록 하겠습니다.

연습

  • 모두 다른 유형의 light와 fragment shader를 사용하여 실험해보세요. 벡터를 뒤집어보거나 > 대신에 < 기호를 사용해보세요. 출력되는 효과에 대해 설명해보세요.



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

반응형