OpenGL "Light Casters"
[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로 설정했다