OpenGL Study

OpenGL "Light Casters"

lvneux 2023. 1. 20. 20:48

[LIGHT CASTERS]

- 지금까지 사용한 모든 lighting은 공간에 하나의 점으로 나타나는 하나의 광원으로 인한 것이다

: 그러나 현실에서는 여러 유형의 빛이 존재한다

- light cater : object에 빛을 발하는 광원

[DIRECTIONAL LIGHT]

- directional light : 광원이 무한히 멀리 있을 때 모든 광선들이 동일한 방향을 가지는 것

 - directional light의 예로는 태양이 있다

: 태양은 우리와 무한히 멀리 있지는 않지만 lighting 계산에서는 무한히 멀리 있다고

  간주할 만큼 우리와 멀리 떨어져 있다

: 좌측의 이미지와 같이 태양에서 오는 모든 광선들은 서로 평행하다

- 모든 광선들이 평행하기 때문에 각 object들이 광원의 위치와 어떠한 관계가 있는지에

  대해서는 상관이 없다

: 빛의 방향은 scene에 존재하는 각각의 object에 모두 동일하기 때문이다

: 빛의 방향 vector가 동일하므로 scene의 각 object에 대한 lighting 계산이 유사하다

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

- 빛에 대한 위치 vector 대신에 방향 vector를 정의하면 directional light를 만들 수 있다

- shader 계산은 대부분 동일하지만 이번 파트에서는 빛의 position을 사용하여 계산된 lightDir 대신에 빛의 direction을 직접적으로 사용한다

- 지금까지의 lighting 계산들은 fragment로부터 광원으로 향하는 빛을 원했지만, 사용자들은 일반적으로  directional light를 광원으로부터 fragment로 향하는 방향으로 나타내는 것을 선호하기 때문에 light direction vector의 부호를 바꿔 사용한다

: 따라서 현재 생성한 방향 vector는 광원을 향하고 있다

- 최종 lightDir vector는 diffuse, specular 계산에 사용된다

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);
}

- Directional light가 모든 object들에 있어서 동일한 효과를 가진다는 것을 명확히 보여주기 위해 coordinate system 파트에서 사용했던 컨테이너 파티 scene을 다시 사용한다

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

- 광원의 방향을 실제로 지정한다

: 지금은 광원의 방향을 광원으로부터의 방향으로 정의한다 (빛이 아래쪽을 향하고 있다)

-  우리는 빛의 위치와 방향을 vec3 타입으로 지정해왔지만, 사용자에 따라 vec4 타입으로 정의하기도 한다

: 위치 vector를 vec4로 정의할 때, w요소를 1.0으로 설정하여 이동과 projection이 올바르게 적용되도록 해야한다

: 방향 vector를 vec4로 정의할 때는 이동 변환이 영향을 미치지 않아야하므로 w요소를 0.0으로 정의해야한다

: 방향 vector는 vec4(-0.2f, -1.0f, -0.3f, 0.0f)와 같이 표현된다

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 (as in previous chapters)

 

 - vec4 타입의 vector는 빛의 유형을 판단하는 함수 역할을 하기도 한다

: w요소가 1.0이면 빛의 위치 vector이다

: w요소가 0.0이면 빛의 방향 vector이다

- 지금까지의 코드들을 실행하면 위와 같은 결과를 확인할 수 있다

 

[POINT LIGHTS]

1. Before

 

 

 - Point light

: world의 어딘가에 주어진 위치를 찾는 광원

: 모든 방향으로 빛을 밝히고 거리에 따라 광선이 희미해진다 (전구, 횃불 등)

- 광원에 대한 각 object의 거리를 비교해서 빛을 받는 정도를 조절한다

 

 

 

2. Attenuation (감쇠)

- Attenuation  : 광선이 지나가는 거리에 따라 빛의 세기를 줄이는 것

- 위 공식을 사용하여 광원과 fragment 사이의 거리를 기반으로 하는 attenuation 값을 계산한다

- d = fragment에서 광원까지의 거리를 나타낸다

- Kc = 최종 결과의 분모를 1보다 작지 않게 만들기 위한 상수항으로, 일반적으로 1.0을 유지한다

: 최종 결과의 분모가 1보다 작으면 특정 거리에서 빛의 세기를 증폭시켜 원하는 효과를 낼 수 없다

- Kl = 거리 값과 곱해져 1차원 방법으로 세기를 감소시키는 1차항이다

- Kq =  거리의 사분면과 곱해져 2차원적으로 광원의 세기를 감소시키는 2차항으로, 거리에 따라 중요도가 달라진다

: 거리가 가까울 때 이 항은 1차항에 비해 덜 중요하고, 거리가 멀 때는 1차항보다 중요해진다

 - 2차항이 1차항을 능가할 정도로 거리가 충분히 커질 때까지 빛의 세기는

   1차원적인 방법으로 빠르게 감소된다

: 최종적인 효과는 빛이 가까운 범위 내에 있을 때 상당히 밝고 거리에 따라

  빠르게 어두워지며 이후에는 점점 느린 속도로 어두워지게 되는 효과이다

- 좌측의 그래프는 100크기의 거리에서 이러한 attenuation이 가지는 효과를

  도식화한 그래프다

: 거리가 작을 때 높은 세기를 가지고 거리가 커질수록 세기가 상당히 많이

  줄어들고 느리게 0으로 다가간다

 

3. Choosing the right values

 

 

 

- 좌측의 표는 특정한 반지름(거리)를 커버하는 일종의 현실적인 광원을 시뮬레이션 하기 위해 가질 수 있는 항들의 값을 포함한다

: 첫 번째 열은 주어진 항들을 사용해서 빛이 커버할 수 있는 거리를 지정한다

- 상수항 Kc는 모든 경우에서 1.0을 유지한다

- 1차항 Kl과 2차항 Kq는 일반적으로 큰 거리를 커버하기 위해서는 아주 작은 값을 가진다

- 우리에게는 32~100의 거리면 충분하다

 

 

 

4. Implementing attenuation

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

- Attenuation을 구현하기 위해 fragment에 추가적으로 constant, linear, quadratic 항이 필요하다

: 이 항들은 light struct 안에 저장한다

: 이전 장에서 했던 방식으로 lightDir을 계산한다 (Directional light 섹션 X)

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

- main 프로그램에서 각 항의 값들을 설정한다

: Choosing the right values에서의 표를 참고하여 값을 설정한다

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;

- fragment shader에서 attenuation을 구현하는 방법은 간단하다

: 공식을 기반으로 attenuation 값을 계산하고 이 값을 ambient, diffuse, specular component에 곱해 구현한다

- 광원까지의 거리는 fragment와 광원 사이의 거리를 얻음으로써 거리 값을 얻을 수 있다

: GLSL의 length 함수를 사용해 vector의 길이를 구한다

- 그 후 attenuation 값을 각각 ambient, diffuse, specular에 곱한다

- 지금까지의 코드를 실행한 출력 결과이다

 

[SPOTLIGHT]

1. Spotlight

 - spotlight

: world의 어딘가에 위치한 광원 (가로등, 손전등 등)

: 특정 방향으로만 광선을 쏜다

: spotlight 방향의 특정 반지를 내부에 있는 object만 밝아지고 나머지는 어두워진다

- OpenGL의 Spotlight는 world-space에서의 위치, 방향, cutoff 각으로 나타내어진다

: 각 fragment에 대해 spotlight의 cutoff 방향 사이에(원뿔 내부에) 있는지를 계산하고, 

  그렇다면 그에 맞춰서 fragment를 밝힌다

- LightDir : fragment에서 광원까지의 방향을 나타내는 vector이다

- SpotDir : spotlight가 겨누고 있는 방향이다

- Phi ϕ : spotlight의 반지름을 지정하는 cutoff 각으로, 이 각 외부에 있는 모든 것들은

  spotlight에 의한 빛을 받지 못한다

- Theta θ : LightDir vector와 SpotDir vector 사이의 각으로, ϕ 값보다 작아야한다

- LightDir과 SpotDir를 내적하여 이를 cutoff 각 ϕ와 비교해야한다

 

2. Flashlight

- flashlight

: viewer의 위치에 있고 일반적으로 사용자의 관점을 향해 똑바로 겨누고 있는 spotlight

: 전형적인 spotlight와 달리 위치와 방향이 사용자의 위치와 방향에 따라 계속해서 업데이트 된다

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

- fragment shader를 위해 필요한 값들은 spotlight의 위치 vector, 방향 vector, cutoff 각이 있다

: 이 값들을 Light struct에 저장한다

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

- main 프로그램에서 적절한 값들을 shader에 넘긴다

- cutoff 값을 각도로 설정하지 않고 각에 대한 cos값을 계산한 후 이 값을 fragment shader로 전달하도록 설정한 이유

: fragment shader에서 LightDir과 SpotDir의 내적을 계산하고 있는데 내적은 각이 아닌 cos 값을 반환하기 때문에, cutoff 각을 각도로 설정하면 두 값을 직접적으로 비교할 수 없기 때문이다

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

- theta 값을 계산하고 이를 cutoff 값과 비교하여 spotlight 내부에 있는지 외부에 있는지 판단한다

- lightDir vector와 부호를 바꾼 direction vector의 내적을 계산하고, 관련된 모든 vector들을 정규화했는지 확인한다

 

- if 조건식에 <가 아닌 >를 쓴 이유

: 각은 cos 값으로 나타내졌으므로 좌측의 그래프와 같이 각이 0이면 cos 1.0으로

  나타내어지고, 각이 90이면 cos 0.0으로 나타내어지기 때문이다

- theta가 cutoff값보다 커야하는 이유

: cutoff 값은 현재 cos 12.5로 설정되어 있는데, 이는 0.9978과 동일하므로 cos theta 값이 0.9979와 1.0 사이에 있어야 spotlight 내부에 fragment가 존재한다는 것을 의미한다

 

- 지금까지의 코드를 실행한 출력 결과이다

: spotlight의 외곽선이 너무 뚜렷해 부자연스러워 보인다

3. Smooth/Soft edges

- 외곽선을 부드럽게 하기 위해 inner 원뿔과 outer 원뿔을 가지는 spotlight를 시뮬레이션 해야한다

: 내부 원뿔은 이전과 동일하게, 외부 원뿔은 내부에서 외부로 갈 수록 점점 빛이 어두워지는 효과를 갖도록 할 것이다

- 위 공식을 사용하여 spotlight의 방향 vector와 외부 원뿔의 vector 사이의 각에 대한 cos 값을 정의하고, fragment가 내부 원뿔과 외부 원뿔 사이에 있으면 빛의 세기 값을 0.0~1.0 사이로 계산한다

: fragment가 내부 원뿔 안에 존재한다면 빛의 세기는 1.0이고, 외부 원뿔 바깥에 존재한다면 0.0이다

- 위 공식에서 ϵ은 내부 원뿔과 외부 원뿔 사이의 차이다 (ϵ=ϕγ)

- 최종 결과 I는 현재 fragment의 spotlight 빛의 세기 값이다

- 위에서 사용한 공식이 어떻게 동작하는 지를 시각화하기 위해 사용하는 여러 샘플값을 도식화한 표이다

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

- 빛의 세기 값을 적절히 고정시켜 fragment shader에 if-else구문을 없앤 후, 계산된 세기 값을 light component에 곱해준다

- 0.0과 1.0 사이의 첫 번째 parameter를 clamp하는 clamp function을 사용한다

: 세기 값을 0.0 ~ 1.0 사이로 유지할 수 있다

- outerCutoff 값을 light struct에 추가하고, 메인 프로그램에서 이 값의 uniform 값을 설정한다

- 최종 출력 결과

: innerCutoff 값은 12.5, outerCutoff 값은 17.5로 설정했다