Skip to content

다양한 광원 모델

지금까지 우리가 사용한 모든 조명은 공간상의 한 지점, 즉 단일 광원에서 나온 것이었습니다. 이는 좋은 결과를 보여주지만, 실제 세계에는 각각 다른 방식으로 작용하는 여러 유형의 빛이 존재합니다. 이러한 다양한 종류의 광원들을 광원 모델(light caster) 이라고 합니다. 이 장에서는 다양한 유형의 광원에 대해 논의할 것입니다. 다양한 광원을 시뮬레이션하는 방법을 배우는 것은 여러분의 환경을 더욱 풍부하게 만들어 줄 또 하나의 유용한 도구가 될 것입니다.

먼저 방향광에 대해 알아보고, 그다음에는 방향광의 확장 개념인 점광원, 마지막으로 스포트라이트에 대해 알아보겠습니다. 다음 장에서는 이러한 다양한 조명 유형을 하나의 장면에 결합하는 방법을 살펴보겠습니다.

방향성 조명(방향광)

광원이 멀리 떨어져 있을 때, 광원에서 나오는 광선들은 거의 평행합니다. 따라서 물체나 관찰자의 위치에 관계없이 모든 광선이 같은 방향에서 오는 것처럼 보입니다. 광원이 무한히 멀리 떨어져 있는 것으로 모델링될 때, 모든 광선이 같은 방향을 가지는 방향성 광원이라고 합니다. 이는 광원의 위치와 무관합니다.

방향성 광원의 좋은 예는 우리가 알고 있는 태양입니다. 태양은 우리에게서 무한히 멀리 떨어져 있는 것은 아니지만, 조명 계산에서는 마치 무한히 멀리 떨어져 있는 것처럼 인식됩니다. 따라서 태양에서 나오는 모든 광선은 다음 이미지에서 볼 수 있듯이 평행 광선으로 모델링됩니다.

모든 광선이 평행하기 때문에 장면 내 모든 물체와 광원 위치과의 관계는 중요하지 않습니다. 빛의 방향 벡터가 동일하게 유지되므로 장면 내 모든 물체에 대한 조명 계산은 유사합니다.

우리는 위치 벡터 대신 광원 방향 벡터를 정의함으로써 이러한 방향성 광원을 모델링할 수 있습니다. 셰이더 계산은 대부분 동일하지만, 이번에는 광원의 위치 벡터를 사용하여 lightDir 벡터를 계산하는 대신 광원의 방향 벡터를 직접 사용합니다.

struct Light {
    // vec3 position; // 방향광을 사용할 때는 더 이상 필요하지 않습니다.
    vec3 direction;

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
[...]
void main()
{
  vec3 lightDir = normalize(-light.direction);
  [...]
}

먼저 light.direction 벡터를 반전시켜야 한다는 점에 유의하세요. 지금까지 사용한 조명 계산은 광원의 방향이 프래그먼트에서 광원을 향하는 방향이라고 가정하지만, 일반적으로 방향광은 광원에서 시작하는 전역 방향으로 지정하는 것을 선호합니다. 따라서 전역 광원 방향 벡터의 방향을 반전시켜야 합니다. 이제 이 벡터는 광원을 향하는 방향 벡터가 됩니다. 또한 입력 벡터가 단위 벡터라고 가정하는 것은 바람직하지 않으므로 벡터를 정규화하는 것을 잊지 마세요.

이렇게 얻은 lightDir 벡터는 이전과 마찬가지로 확산광 및 반사광 계산에 사용됩니다.

방향광이 여러 객체에 동일한 효과를 미친다는 것을 명확히 보여주기 위해 좌표 공간 챕터 마지막 ​​부분에 나왔던 컨테이너 파티 장면을 다시 살펴보겠습니다. 혹시 파티 장면을 놓치셨을까 봐 설명드리자면, 저희는 10개의 서로 다른 컨테이너 위치를 정의하고 각 컨테이너마다 다른 모델 행렬을 생성했습니다. 각 모델 행렬에는 해당 로컬 좌표계에서 월드 좌표계로의 변환이 포함되어 있습니다.

for(unsigned int i = 0; i < 10; i++)
{
    glm::mat4 model = glm::mat4(1.0f);
    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으로 설정하는 것이 중요합니다. 하지만 방향 벡터를 vec4로 정의할 때는 변환이 적용되지 않도록 w 성분을 0.0으로 설정합니다. (방향 벡터는 단순히 방향만을 나타내기 때문입니다.)

방향 벡터는 vec4(-0.2f, -1.0f, -0.3f, 0.0f)와 같이 표현할 수 있습니다. 이는 광원의 종류를 쉽게 구분하는 데에도 사용할 수 있습니다. w 성분이 1.0이면 광원의 위치 벡터이고, w가 0.0이면 광원의 방향 벡터임을 알 수 있습니다. 따라서 이에 따라 계산을 조정하면 됩니다.

if(lightVector.w == 0.0) // 참고: 부동 소수점 오류에 주의하세요.
    // 방향광 계산을 수행합니다.
else if(lightVector.w == 1.0)
    // (이전 장에서처럼) 빛의 위치를 ​​이용하여 빛에 대한 계산을 수행합니다.

재미있는 사실: 이것은 실제로 이전 OpenGL(고정 기능 버전)에서 광원이 방향성 광원인지 위치성 광원인지를 판단하고 그에 따라 조명을 조정하는 방식이었습니다.

이제 애플리케이션을 컴파일하고 장면을 살펴보면 마치 태양과 같은 광원이 모든 물체에 빛을 비추는 것처럼 보일 것입니다. 확산광과 반사광 성분이 모두 하늘 어딘가에 광원이 있는 것처럼 반응하는 것을 볼 수 있나요? 대략 이런 모습일 겁니다.

여기에서 애플리케이션의 전체 소스 코드를 찾을 수 있습니다.

포인트 라이트(점광원)

방향광은 장면 전체를 비추는 전역광에 유용하지만, 일반적으로 장면 곳곳에 여러 개의 점광원을 배치하는 것이 좋습니다. 점광원은 가상 세계의 특정 위치에 고정되어 모든 방향으로 빛을 발하는 광원으로, 빛은 거리에 따라 약해집니다. 전구나 손전등을 점광원으로 생각하면 이해하기 쉽습니다.

앞 장에서는 단순한 점광원을 다루었습니다. 특정 위치에 광원이 있고, 그 광원에서 모든 방향으로 빛이 산란된다고 가정했습니다. 하지만 우리가 정의한 광원은 광선이 절대 사라지지 않도록 시뮬레이션하여 광원이 매우 강한 것처럼 보이게 했습니다. 대부분의 3D 응용 프로그램에서는 전체 장면이 아닌 광원 근처 영역만 비추는 광원을 시뮬레이션하는 것이 바람직합니다.

이전 장에서 다룬 조명 장면에 10개의 컨테이너를 추가하면 맨 뒤쪽 컨테이너가 광원 바로 앞에 있는 컨테이너와 동일한 강도로 조명되는 것을 알 수 있습니다. 아직 거리에 따라 빛의 강도가 감소하는 로직이 구현되지 않았기 때문입니다. 우리는 광원에 가까운 컨테이너에 비해 맨 뒤쪽 컨테이너가 약간만 밝게 조명되도록 하고 싶습니다.

감쇠

빛이 이동하는 거리에 따라 빛의 세기가 감소하는 현상을 일반적으로 감쇠(Attenuation)라고 합니다. 빛의 세기를 거리에 따라 줄이는 한 가지 방법은 선형 방정식을 사용하는 것입니다. 이러한 방정식은 거리에 따라 빛의 세기를 선형적으로 감소시켜 멀리 있는 물체가 덜 밝게 보이도록 합니다. 그러나 이러한 선형 함수는 현실과 다소 동떨어져 보입니다. 실제 세계에서 빛은 가까이 있을 때는 매우 밝지만, 광원의 밝기는 멀어질수록 급격히 감소하고, 남은 빛의 세기는 거리에 따라 서서히 감소합니다. 따라서 빛의 세기를 줄이는 데에는 다른 방정식이 필요합니다.

다행히도 똑똑한 사람들이 이미 이 문제를 해결해 주었습니다. 다음 공식은 광원으로부터 프래그먼트까지의 거리를 기준으로 감쇠 값을 계산하고, 이 값을 나중에 빛의 강도 벡터와 곱합니다.

\[ \begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation} \]

여기서 \(d\)는 프래그먼트에서 광원까지의 거리를 나타냅니다. 감쇠 값을 계산하기 위해 상수항 \(K_c\), 선형항 \(K_l\), 이차항 \(K_q\)의 세 가지 (설정 가능한) 계수를 정의합니다.

  • 상수항은 보통 1.0으로 유지되는데, 이는 분모가 1보다 작아지지 않도록 하기 위한 것입니다. 분모가 1보다 작아지면 특정 거리에서 강도가 증폭되는데, 이는 우리가 원하는 효과가 아니기 때문입니다.
  • 선형항은 거리 값과 곱해져 강도를 선형적으로 감소시킵니다.
  • 이차항은 거리의 제곱에 곱해져 광원의 강도가 제곱에 비례하여 감소하도록 합니다. 거리가 짧을 때는 이차항이 선형항에 비해 중요하지 않지만, 거리가 멀어질수록 그 영향이 훨씬 커집니다.

이차항 때문에 빛은 거리가 충분히 멀어져 이차항이 선형항을 넘어설 때까지는 대부분 선형적으로 감소하다가, 그 이후에는 빛의 강도가 훨씬 빠르게 감소합니다. 결과적으로 가까운 거리에서는 빛이 매우 강하지만, 거리가 멀어질수록 밝기가 급격히 감소하다가 결국에는 더 느린 속도로 밝기가 감소하는 현상이 나타납니다. 다음 그래프는 이러한 감쇠 현상이 100m 거리에서 어떻게 나타나는지를 보여줍니다.

거리가 가까울수록 빛의 강도가 가장 높지만, 거리가 멀어질수록 강도가 크게 감소하여 약 100m 지점에서 0에 도달하는 것을 볼 수 있습니다. 이것이 바로 우리가 원하는 결과입니다.

적절한 값을 선택하기

그렇다면 이 세 가지 항은 어떤 값으로 설정해야 할까요? 적절한 값을 설정하는 것은 환경, 광원이 비추고자 하는 거리, 광원의 종류 등 여러 요인에 따라 달라집니다. 대부분의 경우, 경험과 약간의 조정을 통해 최적의 값을 찾을 수 있습니다. 다음 표는 특정 반경(거리)을 비추는 현실적인 광원을 시뮬레이션하기 위해 이 항들이 가질 수 있는 몇 가지 값을 보여줍니다. 첫 번째 열은 주어진 항으로 광원이 비추는 거리를 나타냅니다. 이 값들은 Ogre3D 위키에서 가져온 것으로, 대부분의 광원에 적용하기 좋은 시작점입니다.

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으로 유지됩니다. 선형항 \(K_l\)은 일반적으로 더 먼 거리를 커버하기 위해 상당히 작은 값으로 설정되며, 이차항 \(K_q\)는 더욱 작은 값으로 설정됩니다. 구현 시 이러한 값들이 어떤 영향을 미치는지 직접 실험해 보시기 바랍니다. 저희 환경에서는 대부분의 조명에 대해 32~100 정도의 거리면 충분합니다.

감쇠 구현하기

감쇠를 구현하려면 프래그먼트 셰이더에 상수항, 선형항, 이차항 등 세 가지 추가 값이 필요합니다. 이 값들은 이전에 정의한 Light 구조체에 저장하는 것이 가장 좋습니다. 참고로, 이것은 방향광이 아닌 점광원이므로 (이전 장에서처럼) 위치를 사용하여 lightDir을 다시 계산해야 합니다.

struct Light {
    vec3 position;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;

    float constant;
    float linear;
    float quadratic;
}; 

다음으로 응용 프로그램에서 조건을 설정합니다. 빛이 50의 거리를 커버하도록 하려면 표에서 적절한 상수항, 선형항, 이차항을 사용합니다.

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

프래그먼트 셰이더에서 감쇠를 구현하는 것은 비교적 간단합니다. 단순히 방정식에 따라 감쇠 값을 계산하고 이 값을 주변광, 확산광 및 반사광 구성 요소에 곱하면 됩니다.

방정식이 제대로 작동하려면 광원까지의 거리가 필요합니다. 벡터의 길이를 계산하는 방법을 기억하시나요? 프래그먼트와 광원 사이의 차이 벡터를 계산하고 그 결과 벡터의 길이를 취하면 거리 항을 얻을 수 있습니다. 이를 위해 GLSL의 내장 함수인 length를 사용할 수 있습니다.

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

그런 다음 이 감쇠 값을 주변광, 확산광 및 반사광 색상에 곱하여 조명 계산에 포함시킵니다.

주변광 성분을 그대로 두면 거리에 따라 주변 조도가 감소하지 않지만, 광원이 두 개 이상 사용될 경우 모든 주변광 성분이 누적되기 시작합니다. 이 경우에는 주변광도 함께 감쇠시켜야 합니다. 환경에 가장 적합한 설정을 찾기 위해 여러 가지 시도를 해보세요.

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

애플리케이션을 실행하면 다음과 같은 결과가 나타납니다.

현재 앞쪽 컨테이너들만 색이 보이고, 가장 가까운 컨테이너가 가장 밝은 것을 볼 수 있습니다. 뒤쪽 컨테이너들은 광원에서 너무 멀리 떨어져 있어 전혀 색깔이 보이지 않습니다. 애플리케이션의 소스 코드는 여기에서 확인할 수 있습니다.

따라서 점광원은 조명 계산에 위치와 감쇠를 설정할 수 있는 광원입니다. 조명 도구에 추가되는 또 다른 유형의 광원인 셈입니다.

스포트라이트

마지막으로 살펴볼 조명은 스포트라이트입니다. 스포트라이트는 주변 환경의 특정 위치에 설치된 광원으로, 모든 방향으로 빛을 비추는 대신 특정 방향으로만 빛을 비춥니다. 따라서 스포트라이트가 비추는 방향을 중심으로 일정 반경 내에 있는 물체만 밝게 빛나고, 그 외의 부분은 어둡게 유지됩니다. 가로등이나 손전등이 스포트라이트의 좋은 예입니다.

OpenGL에서 스포트라이트는 월드 공간 위치, 방향, 그리고 스포트라이트의 반경을 지정하는 차단 각도로 표현됩니다. 각 프래그먼트에 대해 해당 프래그먼트가 스포트라이트의 차단 방향(즉, 원뿔형 범위) 내에 있는지 계산하고, 만약 그렇다면 해당 프래그먼트에 조명을 비춥니다. 다음 이미지는 스포트라이트의 작동 방식을 보여줍니다.

  • LightDir: 프래그먼트에서 광원을 가리키는 벡터입니다.
  • SpotDir: 스포트라이트가 향하는 방향.
  • 파이(\(\phi\)): 스포트라이트의 조사 반경을 지정하는 차단 각도. 이 각도 밖의 영역은 스포트라이트의 조명을 받지 않습니다.
  • 세타(\(\theta\)): 조명 방향 벡터와 스포트라이트 방향 벡터 사이의 각도. 스포트라이트 영역 안에 있으려면 \(\theta\) 값이 \(\phi\)보다 작아야 합니다.

그러니까 기본적으로 우리가 해야 할 일은 LightDir 벡터와 SpotDir 벡터의 내적(두 단위 벡터 사이의 각도의 코사인 값)을 계산하고 이를 차단 각도 \(\phi\)와 비교하는 것입니다. 이제 스포트라이트가 무엇인지 (어느 정도) 이해하셨으니, 손전등 형태로 스포트라이트를 만들어 보겠습니다.

손전등

손전등은 플레이어의 위치에 있는 스포트라이트로, 일반적으로 플레이어 시점에서 정면을 향합니다. 손전등은 기본적으로 일반 스포트라이트와 같지만, 플레이어의 위치와 방향에 따라 위치와 방향이 지속적으로 업데이트됩니다.

따라서 프래그먼트 셰이더에 필요한 값은 스포트라이트의 위치 벡터(프래그먼트-라이트 간 방향 벡터를 계산하기 위해), 스포트라이트의 방향 벡터 및 차단 각도입니다. 이러한 값은 Light 구조체에 저장할 수 있습니다.

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

다음으로 적절한 값을 셰이더에 전달합니다.

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

보시다시피, 차단 값에 대한 각도를 직접 설정하는 대신, 각도를 기반으로 코사인 값을 계산하고 그 결과를 프래그먼트 셰이더에 전달합니다. 그 이유는 프래그먼트 셰이더에서 LightDir 벡터와 SpotDir 벡터의 내적을 계산하는데, 내적은 각도가 아닌 코사인 값을 반환하기 때문입니다. 각도와 코사인 값은 직접 비교할 수 없습니다. 셰이더에서 각도를 얻으려면 내적 결과의 역코사인 값을 계산해야 하는데, 이는 비용이 많이 드는 연산입니다. 따라서 성능을 향상시키기 위해 주어진 차단 각도의 코사인 값을 미리 계산하여 이 결과를 프래그먼트 셰이더에 전달합니다. 이제 두 각도 모두 코사인 값으로 표현되므로, 비용이 많이 드는 연산 없이 직접 비교할 수 있습니다.

이제 남은 것은 세타 \(\theta\) 값을 계산하고 이를 차단 \(\phi\) 값과 비교하여 우리가 스포트라이트 영역 안에 있는지 밖에 있는지를 판단하는 것입니다.

float theta = dot(lightDir, normalize(-light.direction));

if(theta > light.cutOff) 
{       
  // 조명 계산을 수행합니다
}
else  // 만약 밖에 있다면, 스포트라이트 바깥 부분이 완전히 어둡지 않도록 주변광을 사용하세요.
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

먼저 광원 방향 벡터와 부호를 반전시킨 방향 벡터(벡터가 광원에서 나오는 방향이 아닌 광원을 향하도록 하기 위함)의 내적을 계산합니다. 관련된 모든 벡터를 정규화해야 합니다.

if 문에 < 기호 대신 > 기호가 있는 이유가 궁금하실 수 있습니다. 스포트라이트 안에 들어가려면 세타 값이 광원의 차단 값보다 작아야 하지 않을까요? 맞습니다. 하지만 각도 값은 코사인 값으로 표현된다는 점을 잊지 마세요. 0도는 코사인 값 1.0으로, 90도는 코사인 값 0.0으로 표현됩니다. 아래 식을 참고하세요.

이제 코사인 값이 1.0에 가까울수록 각도가 작아지는 것을 알 수 있습니다. 따라서 세타 값이 차단 값보다 커야 하는 이유가 이해됩니다. 현재 차단 값은 코사인 값 12.5, 즉 0.976으로 설정되어 있으므로 코사인 세타 값이 0.976에서 1.0 사이이면 해당 프래그먼트가 마치 스포트라이트 안에 있는 것처럼 밝게 빛나게 됩니다.

애플리케이션을 실행하면 스포트라이트 원뿔 안쪽에 있는 프래그먼트들만 밝게 비추는 스포트라이트가 나타납니다. 다음과 같은 모습이 될 것입니다.

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

하지만 여전히 약간 부자연스러워 보입니다. 특히 스포트라이트의 가장자리가 너무 날카롭기 때문입니다. 빛이 스포트라이트 원뿔의 가장자리에 닿으면 부드럽게 사라지는 대신 완전히 꺼져 버립니다. 실제 스포트라이트라면 가장자리로 갈수록 빛이 서서히 줄어들겠죠.

매끄럽거나 부드러운 모서리

부드러운 가장자리를 가진 스포트라이트 효과를 내려면 내부 원뿔과 외부 원뿔을 ​​가진 스포트라이트를 시뮬레이션해야 합니다. 내부 원뿔은 이전 섹션에서 정의한 원뿔로 설정할 수 있지만, 외부 원뿔은 내부에서 외부 원뿔의 가장자리로 갈수록 빛이 점차 약해지도록 해야 합니다.

외부 원뿔을 ​​생성하려면 스포트라이트의 방향 벡터와 외부 원뿔 벡터(반지름과 동일) 사이의 각도를 나타내는 또 다른 코사인 값을 정의하면 됩니다. 그러면 프래그먼트가 내부 원뿔과 외부 원뿔 사이에 있으면 0.0에서 1.0 사이의 강도 값을 계산해야 합니다. 프래그먼트가 내부 원뿔 안에 있으면 강도는 1.0이고, 외부 원뿔 밖에 있으면 0.0입니다.

다음 방정식을 사용하여 해당 값을 계산할 수 있습니다.

\[ \begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation} \]

여기서 \(\epsilon\)(엡실론)은 내부 원뿔(\(\phi\))과 외부 원뿔(\(\gamma\)) 사이의 코사인 차이(\(\epsilon = \phi - \gamma\))입니다. 결과적으로 얻어지는 \(I\) 값은 현재 프래그먼트에서의 스포트라이트 강도입니다.

이 공식이 실제로 어떻게 작동하는지 시각화하기가 다소 어려우므로 몇 가지 예시 값을 사용하여 직접 시도해 보겠습니다.

\(\theta\) \(\theta\) (도 단위) \(\phi\) (내부 차단 각도) \(\phi\) (도 단위) \(\gamma\) (외부 차단 각도) \(\gamma\) (도 단위) \(\epsilon\) \(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\)

보시다시피, 우리는 기본적으로 \(\theta\) 값을 기준으로 외부 코사인과 내부 코사인 사이를 보간하고 있습니다. 만약 아직도 잘 이해가 안 되신다면 걱정하지 마세요. 그냥 공식을 당연하게 여기고, 나중에 훨씬 더 나이가 들고 현명해졌을 때 다시 오셔도 됩니다.

이제 우리는 스포트라이트 바깥에서는 음수, 내부 원뿔 안쪽에서는 1.0보다 큰 값, 그리고 가장자리 부근에서는 그 중간 값을 갖는 강도 값을 얻었습니다. 값을 적절하게 제한하면 프래그먼트 셰이더에서 if-else 문이 더 이상 필요하지 않으며, 단순히 광원 구성 요소에 계산된 강도 값을 곱하면 됩니다.

float theta     = dot(lightDir, normalize(-light.direction));
float epsilon   = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);    
...
// 주변 조명은 그대로 두어 항상 약간의 빛이 있도록 하겠습니다.
diffuse  *= intensity;
specular *= intensity;
...

첫 번째 인자값을 0.0과 1.0 사이의 값으로 제한(clamp)하는 clamp 함수를 사용한다는 점에 주의하세요. 이를 통해 강도(intensity) 값이 [0, 1] 범위를 벗어나지 않도록 보장할 수 있습니다.

Light 구조체에 outerCutOff 값을 추가하고 애플리케이션에서 유니폼 값을 설정해야 합니다. 다음 이미지에서는 내부 차단 각도 12.5도, 외부 차단 각도 17.5도를 사용했습니다.

아, 훨씬 낫네요. 내부 및 외부 차단 각도를 조정해 보면서 필요에 더 적합한 스포트라이트를 만들어 보세요. 애플리케이션 소스 코드는 여기에서 확인할 수 있습니다.

이러한 손전등/스포트라이트 형태의 램프는 공포 게임에 완벽하며, 방향성 조명 및 점광원과 함께 사용하면 주변 환경이 훨씬 더 밝아질 것입니다.

연습 문제

  • 다양한 광원 유형과 해당 프래그먼트 셰이더를 모두 실험해 보세요. 일부 벡터를 반전시키거나 > 대신 <를 사용해 보세요. 각기 다른 시각적 결과를 설명해 보세요.

Comments