본문 바로가기

Game/Graphics

Learn OpenGL - Lighting : Light casters

link : https://learnopengl.com/Lighting/Light-casters


Light casters


 지금까지 사용했던 모든 조명은 공간의 단일 지점인 단일 광원에서 발생했다.


그것은 좋은 결과를 주지만 현실 세계에서 우리는 각각 다른 행동을 하는 몇 가지


유형의 빛을 가지고 있다. 물체에 빛을 비추는 광원을 light caster라고 한다.


이 튜토리얼에서는 여러 가지 유형의 light caster에 대해 설명한다.


다양한 광원을 시뮬레이트하는 방법은 도구 상자에서 환경을 더욱 풍부하게하는


또 하나의 도구이다.



 우리는 우선 방향성 빛에 대해 이야기 할 것이고, 그 다음에는 우리가 이전에 가지고


있었던 것의 연장선인 점등과 마지막으로 우리는 spotlight에 대해 토론할 것이다.


다음 튜토리얼에서는 이러한 다양한 조명 유형을 하나의 장면으로 결합한다.






Directional Light


 광원이 멀리 떨어져있을때, 광원으로부터 나오는 광선은 서로 평행하다.


물체 및 관찰자의 위치에 관계없이 모든 광선이 같은 방향에서 오는 것처럼 보인다.


광원이 무한히 멀리 모델링된 경우 모든 광선이 동일한 방향을 가지기 때문에


지향성 광원이라고 부른다. 그것은 광원의 위치에 독립적이다.



 Directional Light의 좋은 예는 우리가 알고 있는 태양이다. 태양은 우리로부터


무한히 떨어져 있지는 않지만 조명 계산에서는 무한히 떨어져 있는 것으로


인식할 수 있을만큼 멀리 떨어져 있다. 태양으로부터의 모든 광선은 다음 이미지에서


볼 수 있는 것처럼 평행 광선으로 모델링된다:




 모든 광선이 평행하기 때문에 조명 방향이 장면의 각 객체에 대해 동일하게 유지되기


때문에 각 객체가 광원의 위치와 어떻게 관련되는지는 중요하지 않다. 광원의 방향 벡터는


동일하게 유지되므로 조명 계산은 장면의 각 오브젝트에 대해 비슷하다.



 우리는 위치 벡터가 아닌 조명 방향 벡터를 정의함으로써 그러한 방향성 빛을 모델링할 수 있다.


쉐이더 계산은 빛의 위치 벡터로 LightDir 벡터를 계산하는 대신 Light의 방향 벡터를 직접


사용하는 것을 제외하고는 거의 동일하다.

struct Light {
    // vec3 position; // No longer necessery when using directional lights.
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};
...
void main()
{
  vec3 lightDir = normalize(-light.direction);
  ...
}

 먼저 light.direction 벡터를 반대 방향으로 바꾼다. 지금까지 사용한 조명 계산은 빛 방향이


조각에서 광원 방향으로 향할 것이라고 기대하지만 사람들은 일반적으로 방향 광원을


광원에서 가리키는 전역 방향으로 지정하는 것을 선호한다. 그러므로 우리는 방향을 바꾸기


위해 지구의 빛의 방향 벡터를 무효화해야한다. 그것은 이제 광원쪽으로 향하는 방향 벡터이다.


또한, 입력 벡터를 단위 벡터로 가정하는 것은 현명하지 않으므로 벡터를 정규화해야한다.



 결과로 생성된 lightDir 벡터는 확산 및 반사 계산에서 이전과 같이 사용된다.



 Directional Light가 모든 여러 객체에 대해 동일한 효과를 발휘한다는 것을 명확히 나타내기 위해


우리는 coordinate system 튜토리얼의 끝에 있는 container position을 다시 봐야한다. 우리가 party를


놓친 경우를 대비해 처음에 10개의 컨테이너 위치를 정의하고, 각 모델 행렬이 적절한 지역-세계 변환을


포함하는 컨테이너마다 다른 모델 행렬을 생성했다:

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으로 설정해 translate 및 projection이 올바르게 적용되도록 하는 것이 중요하다. 그러나 방향 벡터를 vec4로 정의할 때 translate가 효과를 가지기를 원하지는 않는다. 그러면 w 컴포넌트를 0.0으로 정의한다:


if(lightVector.w == 0.0) // note: be careful for floating point errors
  // do directional light calculations
else if(lightVector.w == 1.0)
  // do light calculations using the light's position (like last tutorial)  

 재미있는 사실 : 이것은 실제로 광원이 지향성 광원인지 또는 위치 광원인지를 결정하고 구형 OpenGL(fixed-functionality)이 이를 기반으로 조명을 조정한 방법이다.

 이제 응용 프로그램을 컴파일하고 장면을 통해 비행하는 경우 모든 객체에 햇빛과 같은 광원을


비추는 것처럼 보인다. 확산과 반사 구성 요소가 마치 하늘 어딘가에 광원이 있는 것처럼


보인다:









Point Lights


 Directional light는 전체 장면을 비추는 전역 조명에 적합하지만 이것을 제외하고 일반적으로


장면 전체에 흩어져있는 여러개의 점 광원도 필요하다. 점 광원은 전 세계 어디에서나 주어진


위치의 광원으로, 광선이 먼 거리에서 약해지는 모든 방향으로 조명한다.


전구 및 횃불을 가리키는 빛이되는 바퀴처럼 생각해라.




 이전 튜토리얼에서 우리는 점 광원으로 작업해왔다. 우리는 주어진 위치에 광원을 배치해 주어진 빛의


위치에서 모든 방향으로 빛을 산란시킨다. 그러나 광원이 희미해지지 않는 시뮬레이션된 광선을 정의해


광원처럼 보이게 만든다. 대부분의 3D 시뮬레이션에서 전체 장면이 아닌 광원에 가까운 특정 영역만


조명하는 광원을 시뮬레이트하고 싶다.



 이전 튜토리얼의 조명 장면에 10개의 컨테이너를 추가하면 뒤쪽의 모든 컨테이너가 램프 앞에 있는


컨테이너와 동일한 강도로 켜져 있음을 알 수 있다. 거리에 따라 빛을 감소시키는 공식은 없다.


우리는 광원에 가까운 용기에 비해 뒤쪽의 용기가 약간만 켜지길 원한다.





Attenuation


  광속이 이동하는 거리에 대한 광의 강도를 감소시키는 것은 일반적으로 attenuation(감쇠)로 불린다.


거리에 따른 광 강도를 줄이는 한 가지 방법은 단순히 선형 방정식을 사용하는 것이다.


이러한 방정식은 거리에 따른 빛의 강도를 선형적으로 감소시켜 멀리있는 물체가 덜 밝아지도록한다.


그러나, 그러한 선형 함수는 약간 가짜로 보이는 경향이 있다. 현실 세계에서는 조명이 일반적으로


가까이서 아주 밝게 보이지만 광원의 밝기는 시작시 빠르게 줄어들고 남은 빛의 강도는 거리에 따라


서서히 감소한다. 따라서 우리는 빛의 강도를 줄이기 위한 다른 공식이 필요하다.



 다행히도 일부 현명한 사람들은 이미 우리를 위해 이것을 알아냈다. 다음 수식은 나중에 광원의


강도 벡터를 곱하는 광원에 대한 조각의 거리를 기반으로 감쇠 값을 계산한다:



 여기서 d는 단편에서 광원까지의 거리를 나타낸다. 그런 다음 감쇠 값을 계산하기 위해 3가지 항을


정의한다. 상수 항 Kc, 선형항 KI, 이차항 Kq.



    - 상수 항은 대개 1.0으로 유지된다. 이 항은 결과 분모가 1보다 작아지지 않도록 하기 위해 주로 거기에


      유지된다. 그렇지 않으면 특정 거리로 강도를 높이기 때문에 결과가 1보다 작아지지 않는다.


      이는 우리가 찾고 있는 효과가 아니다.



    - 선형 항은 선형 방식으로 강도를 감소시키는 거리 값과 곱해진다.


    - 이차 항은 거리의 사분면과 곱해지고 광원에 대한 강도의 2차 감소를 설정한다. 이차 항은 거리가 작은


      선형 항에 비해 덜 중요하지만 거리가 커지면 선형 항보다 훨씬 커진다.



 이차항으로 인해 거리가 두 번째 항이 선형 항을 뛰어 넘을 때까지 빛이 선형 방식으로 대부분 감소하고


광도가 훨씬 빠르게 감소한다. 결과적으로 가까운 거리에서 빛이 상당히 강하지만 거리에 따라 밝기가 빠르게


사라지기 때문에 결과적으로 밝기가 느려진다. 다음 그래프는 거리 100에 걸쳐 감쇠가 미치는 영향을 보여준다:




 거리가 가까울 때 빛의 강도가 가장 강하다는 것을 알 수 있다. 그러나 거리가 커지자마자 강도가 현저히 감소하고


거리가 100도로 가까울수록 천천히 강도가 0에 도달한다. 이것이 정확히 우리가 원하는 것이다.






Choosing the right values


 그러나 우리는 이 세가지 용어를 어떠한 가치로 설정할까? 올바른 값을 설정하는 것은 환경, 빛이 원하는 거리,


빛의 유형 등 많은 요소에 따라 달라진다. 대부분의 경우 경험과 적절한 조정의 문제이다. 다음 표는 특정 반경을


다루는 실제 광원을 시뮬레이트하기 위해 이 용어들이 취할 수 있는 몇 가지 값을 보여준다. 첫 번째 열은 빛이


주어진 용어로 커버할 거리를 지정한다. 이 값은 Ogre3D의 위키를 통해 대부분의 조명의 적합한 출발점이다:




 보시다시피 상수 항 Kc는 모든 경우에 1.0으로 유지된다. 선형항 KI는 일반적으로 더 큰 거리를 커버하기에


매우 작으며 이차항 Kq는 훨씬 더 작다. 이러한 값을 조금 실험해보고 구현에 미치는 영향을 확인해라.


우리의 환경에서는 일반적으로 대부분의 조명에 대해 32~100의 거리가 충분하다.








Implementing attenuation


 감쇄를 구현하기 위해 조각 쉐이더에서 세가지 추가값, 즉 수식의 상수, 선형 및 이차 항이 필요하다.


이들의 앞서 정의한 Light 구조체에 저장하는 것이 가장 좋다. 이전 튜토리얼에서와 같이 lightDir을 계산하고


이전의 Directional Light 섹션에서와 같이 계산하지 않는다.

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

 그런 다음 OpenGL에서 용어를 설정한다. 빛이 50의 거리를 커버하기를 원하므로 테이블에서 적절한 상수,


선형 및 2차항을 사용한다:

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

 조각 쉐이더에서 감쇠를 구현하는 것은 비교적 간단합니다. 공식을 기반으로 감쇠 값을 계산하고 이것을


주변, 확산 및 반사 구성 요소에 곱하면된다.



 수식이 작동하려면 광원과의 거리가 필요하다. 벡터의 길이를 계산하는 방법이 기억나나? 우리는 조각과


광원 사이의 차이 벡터를 검색해 결과 벡터의 길이를 취하여 거리 항을 검색할 수 있다. 우리는 GLSL의 내장


길이 함수를 사용해 다음과 같은 목적으로 사용할 수 있다:

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

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

ambient 요소만 남겨두어 주변 조명이 거리에 따라 감소하지는 않지만 1개 이상의 광원을 사용하려면 모든 ambient 구성 요소가 쌓이기 시작하므로 ambient 조명도 감쇠시켜야한다. 환경에 가장 적합한 것을 가지고 놀기만 하면 된다.

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

 응용 프로그램을 실행하면 다음과 같은 내용이 표시된다:



 가장 가까운 컨테이너가 가장 밝은 상태로 전면 컨테이너만 켜져 있음을 알 수 있다. 뒤쪽에 있는 용기는


광원에서 너무 멀리 떨어져 있기 때문에 전혀 켜지지 않는다.



 따라서 점 광원은 조명 계산에 적용 가능한 구성 가능한 위치 및 감쇠가 있는 광원이다.









Spotlight


 우리가 논의할 마지막 유형의 빛은 스포트라이트이다. 스포트라이트는 모든 방향으로 광선을 쏘는 대신


특정 방향으로만 쏘는 환경의 어딘가에 위치하는 광원이다. 그 결과 스포트라이트 방향의 특정 반경 내에 있는


물체만 켜지고, 다른 모든 물체는 어둡게 유지된다. 스포트라이트의 좋은 예는 가로등 또는 손전등이다.



 OpenGL의 스포트라이트는 월드 스페이스 위치, 방향 및 스포트라이트 반경을 지정하는 차단 각도로 표시된다.


조각이 스포트라이트의 컷오프 방향 (즉,원뿔) 사이에 있으면 각 조각에 대해 계산하고, 그렇다면 우리는 그에 따라


조각을 점등한다. 다음 이미지는 스포트라이트가 작동하는 방식에 대한 아이디어를 제공한다.



    - LightDir : 조각에서 광원을 가리키는 벡터이다.


    - SpotDir : 스포트라이트가 목표로하는 방향이다.


    - Phi Φ: 스포트라이트의 반지름을 지정하는 차단 각도이다. 이를 벗어난 모든 것은 스포트라이트에 비춰지지 않는다.


    - Theta θ : LightDir 벡터와 SpotDir 벡터 사이의 각도이다. 스포트라이트 내부에 위치하려면 θ값이 Φ값보다 작아야한다.



 그래서 우리가 기본적으로 해야 할 일은 LightDir 벡터와 SpotDir 벡터 사이의 내적을 계산하고, 이를 컷오프 각도 Φ와


비교하는 것이다. 이제 당신은 스포트라이트가 무엇인지 이해한다. 우리는 손전등의 형태로 그것을 만드려고 한다.









Flashlight


 손전등은 관객의 위치에 위치하고, 일반적으로 플레이어의 관점에서 직면한 스포트라이트이다.


기본적으로 손전등은 일반적인 스포트라이트이지만 플레이어의 위치와 방향에 따라 위치와 방향이


지속적으로 업데이트된다.



 따라서 조각 쉐이더에 필요한 값은 스포트라이트의 위치 벡터 (광원의 방향 벡터 계산),


스포트라이트의 방향 벡터 및 컷오프 각도이다. 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 θ 값을 계산하고, 이것을 컷오프 Φ와 비교해 우리가 스포트라이트 내부에 있는지


또는 외부에 있는지를 결정하는 것이다:

float theta = dot(lightDir, normalize(-light.direction));
    
if(theta > light.cutOff) 
{       
  // do lighting calculations
}
else  // else, use ambient light so scene isn't completely dark outside the spotlight.
  color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);

 우리는 먼저 lightDir 벡터와 negated direction 벡터 사이의 내적을 계산한다. 모든 관련 벡터를 정규화해라.

아마 if 대신 <> 기호가 왜 있는지 궁금할 것이다. 스포트라이트 내부에 빛의 컷오프 값보다 작아서는 안될까? 그렇다, 하지만 각도 값은 코사인 값으로 표시되고 각도 0은 코사인 값 1.0으로 표시되며 90도 각도는 코사인 값 0.0으로 나타난다. 여기에서 볼 수 있다:


이제 코사인 값이 1.0에 가까울수록 각도가 작아짐을 알 수 있다. 이제 왜 theta가 컷오프 값보다 커야하는지 이해할 수 있다. 컷오프 값은 현재 코사인 12.5로 설정되어 0.9978로 0.9979와 1.0 사이의 코사인 세타 값은 조각이 스포트라이트 내부에서 켜지는 결과를 가져온다.

 응용 프로그램을 실행하면 스포트라이트가 발생해 스포트라이트의 원뿔 내부에 directly 조각만 표시된다.


다음과 같이 보일 것이다:



 스포트라이트가 딱딱하기 때문에 여전히 약간 가짜인것처럼 보인다. 조각이 스포트라이트 원뿔의 가장자리에


도달할 때마다 멋진 부드러운 페이드가 없다. 사실적인 스포트라이트는 가장자리를 따라 서서히 빛을 감소시킨다.






Smooth/Soft edges


 부드러운 가장자리가 있는 스포트라이트의 효과를 내기 위해 우리는 내부 콘과 외부 콘을 가진 스포트라이트를


시뮬레이션하려고한다. 우리는 내부 콘을 이전 섹션에서 정의한 원추체로 설정할 수 있지만 외부 콘의 안쪽에서


가장자리까지 빛을 점차 희미하게하는 외부 원추가 필요하다.



 바깥 쪽 원뿔을 만들기 위해 스포트라이트의 방향 벡터와 바깥 원뿔의 벡터 (반지름과 동일) 사이의 각도를


나타내는 또 다른 코사인 값을 정의한다. 그런 다음 조각과 내부 원뿔-외부 원뿔 사이에 있는 경우


0.0과 1.0사이의 강도 값을 계산해야한다. 조각이 내부 원뿔의 안쪽면이면 1.0, 외부 원뿔의 바깥 쪽이면


0.0의 강도이다.



 우리는 다음 공식을 사용해 그러한 값을 계산할 수 있다:



여기서 ϵ 는 내부 ϕ와 외부 원뿔 γ 사이의 코사인 차이이다. (ϵ=ϕ−γ) 결과 I 값은 현재 조각에서


스포트라이트의 강도이다.



 이 수식이 실제로 어떻게 작동하는지 시각화하는 것은 약간 어렵다. 그래서 몇 가지 샘플 값을 사용해 보겠다:


θθ in degreesϕ (inner cutoff)ϕ in degreesγ (outer cutoff)γ in degreesϵI
0.87300.91250.82350.91 - 0.82 = 0.090.87 - 0.82 / 0.09 = 0.56
0.9260.91250.82350.91 - 0.82 = 0.090.9 - 0.82 / 0.09 = 0.89
0.97140.91250.82350.91 - 0.82 = 0.090.97 - 0.82 / 0.09 = 1.67
0.83340.91250.82350.91 - 0.82 = 0.090.83 - 0.82 / 0.09 = 0.11
0.64500.91250.82350.91 - 0.82 = 0.090.64 - 0.82 / 0.09 = -2.0
0.966150.997812.50.95317.50.9978 - 0.953 = 0.04480.966 - 0.953 / 0.0448 = 0.29


 보시다시피 기본적으로 θ 값을 기준으로 외부 코사인과 내부 코사인을 보간한다.


무슨 일이 벌어지고 있는지 아직 알지 못하겠다해도 걱정하지 말아라. 나중에 수식을 다시 보면서 해도 된다.



 스포트라이트 바깥쪽에 있을 때 음의 값을 가지므로 내부의 안쪽과 안쪽 어딘가에 있을때에는 1.0보다 높다.


값을 적절히 clamp하면 조각 쉐이더에서 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);    
...
// we'll leave ambient unaffected so we always have a little light.
diffuse  *= intensity;
specular *= intensity;
...

 첫 번째 인수를 0.0과 1.0 사이의 값으로 클램핑하는 clamp 함수를 사용한다. 이렇게 하면 강도 값이


[0,1] 간격을 벗어나지 않게 된다.



 externalCutOff 값을 Light 구조체에 추가하고 응용 프로그램에서 균일한 값을 설정해야한다.


다음 이미지의 경우 12.5의 내부 차단 각과 17.5의 외부 차단 각도가 사용되었다:



 훨씬 낫다. 안쪽과 바깥쪽 컷오프 각도로 더 나은 스포트라이트를 만들어보아라.


이러한 flash/spotlight 타입의 램프는 공포 게임에 적합하며 방향 및 점등과 결합되어 환경이 실제로 밝아진다.






** ** ** ** ** ** ** ** ** **


삼성 SDS 알고리즘 교육을 와서 그래픽스 공부할 시간이 많이 부족하다.


맥으로 연구실 컴퓨터 팀뷰어 켜서 코딩도 하려니 많이 불편하다.


하지만 조금씩이라도 공부하면서 진도 나가야지!

'Game > Graphics' 카테고리의 다른 글

LearnOpenGL - Glossary  (0) 2018.08.24
Learn OpenGL - Lighting : Multiple lights  (0) 2018.08.24
Learn OpenGL - Lighting : Lighting maps  (0) 2018.08.17
Learn OpenGL - Lighting : Materials  (0) 2018.08.16
Learn OpenGL - Lighting : Basic Lighting  (0) 2018.08.15