Light casters
- 여러 유형의 빛들이 존재.
- 오브젝트에 빛을 캐스트하는 광원을
light caster
라고 한다.
- 오브젝트에 빛을 캐스트하는 광원을
Directional Light
- 멀리 있는 광원에서 나오는 광선은 거의 서로 평행하다.
- 광원이 무한히 멀리 있으면 모두 동일한 방향에서 온다.
- 이를
directional light
라고 부른다
- 모든 광선들은 평행함 => 광원의 위치 정보 필요 없음
대신 방향벡터가 필요.
light.direction
벡터를 추가.- 일반적으로 directional light를 광원으로부터 fragment로 향하는 방향으로 나타내는것을 선
- 그러므로 부호를 바꾸어 반대 방향으로 설정해야함
- 즉, 광원을 향하는 벡터
- 추가적으로 정규화도 해줘야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
//[...]
}
lightDir
벡터는 diffuse, specular 계산에 사용된다.좌표시스템의 예제인 여러 박스가 있는 씬을 사용할것이다.
- 10개의 다른 박스의 위치를 정의했었고. 적절한 변환을 가지고 있는 model 행렬을 생성했음.
1
2
3
4
5
6
7
8
9
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);
}
- 또한 광원의 방향을 지정해야함.
- 지금은 광원으로부터 나오는 아래쪽 방향.
1
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
vec4
타입으로 정의할 수 있다. 이 때위치 벡터
는 ‘w’ 요소를1.0
으로 설정하여 변환이 올바르게 적용되도록 해야하지만, 방향 벡터는 이동변환이 영향을 미치지 않아야하기 때문에w
요소를0.0
으로 정의해야한다. (아핀변환 점과 벡터의 차이)
방향벡터들은
vec4(-0.2f, -1.0f, -0.3f, 0.0f)
과 같이 표현된다. 이는 빛의 유형을 쉽게 판단할 수 있게해준다.w
요소가1.0
이면 빛의 위치벡터이고,0.0
이면 방향벡터이므로 이를 아래와 같이 사용할 수 있다.
1
2
3
4
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)
이는 실제로 광원이 directional light인지 positional light인지 판단하기위해 OpenGL 구버전에서 사용된적이 있었음.
Point Lights
Directional light들은 전체 Scene을 밝히는 전반적인 빛에 사용되는 것이 좋다.
일반적으로 Scene 전체에 산란되는 여러 point light들도 사용한다.
- world의 어딘가에 주어진 위치를 갖는 광원
- 모든 방향으로 빛을 밝힘.
- 거리에 따라 광선은 희미
- 전구나 횃불같음
이전의 내용들이 바로 point light의 간단한 버전
- 이전의 것은 거리 상관없이 구현했음.
- 보통 이와 같이 구현하지 않고, 광원과 가까이에 있는 특정 영역만 밝힘.
거리에 따른 공식을 사용해야한다.
- 광원과 가까이 있는 컨테이너들과 비교해서 멀리 떨어진 컨테이너는 약간의 빛만 받기를 원한다.
Attenuation
- 광선이 지나가는 거리에 따라 빛의 세기를 줄이는 것.
다음 공식은 광원과 fragment 사이의 거리를 기반으로 하는 감쇠값을 계산한다.
- 이 값을 나중에 빛의 세기 벡터에 곱할 것이다.
- d: fragment에서 광원까지의 거리
- 설정가능한 상수항 K_c, linear(1차)항인 K_l, quadratic(2차)항인 K_q
상수항
은 일반적으로 1.0을 유지. 최종 결과의 분모를 1보다 작게 만들지 않도록하기위해 존재. 그렇지 않으면 특정 거리에서 빛의 세기를 증폭시켜 원하는 효과를 낼 수 없다.1차항
: 거리 값과 곱해짐. 1차원 방법으로 세기를 감소시킴2차항
: 거리의 사분면과 곱해짐. 2차원적으로 광원의 세기를 감소. 거리가 가까울 때 이 2차항은 1차항에 비해 덜 중요해짐. 거리가 멀때는 1차항보다 중요해짐.
- 2차항 때문에 거리가 충분히 커질 때까지 빛의 세기는 1차원적인 방법으로 빠르게 감소됨.
- 빛이 가까운 범위내에 있을 때 상당히 밝고, 거리에따라 빠르게 어두워지며, 결국에는 점점 느린 속도로 어두워지게 되는 효과이다.
- 다음 그래프는 이러한 감쇠효과를 보여준다.
- 이 빛은 거리가 작을 때 높은 세기를 가지고, 거리가 커질수록 세기가 상당히 많이 줄어, 느리게 0으로 다가간다.
Choosing the right values
- 3가지 항에대해 올바른 값을 설정해야한다.
- 케이스마다 다르며, 경험이 필요하다.
- 아래의 표는 특정한 반지름(거리)를 커버하는 현실적인 광원을 시뮬레이션하기 위해 가질 수 있는 항들의 값을 보여준다.
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 |
- 위에서 볼수 있는 정보
- 상수항은 항상 1.0이다.
- 1차항, 2차항은 거리가 클수록 꽤 작은 값을 가짐
Implementing attenuation
- 위의 항들을 이제 코드에 넣어보자
- constant, linear, quadratic
1
2
3
4
5
6
7
8
9
10
11
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
- 그다음 값을 설정한다.
1
2
3
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
fragment shader
에서attenuation
을 구현해야한다.- 공식을 그대로 사용하여 계산
- 거리는 간단히 벡터 뺄셈으로 얻는다.
1
2
3
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
- 그런다음 이 감쇄값을 컬러에 곱하여 lighting 계산에 포함시킨다.
- Ambient요소를 건드리지 않고, ambient lighting이 거리에 따라 어두워지지 않게 할 수 있음.
- 하지만 한 개 이상의 광원을 사용하게된다면, ambient 컴포넌트들은 쌓이게됨.
- 이 경우 ambient lighting에도 attenuation을 적용해야함.
- 환경에 따라 최적의 조건으로 설정해야함.
1
2
3
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
- 이제 다음과 같은 결과가 나온다.
- Point light: lighting 계산에 적용되는 위치와 attenuation을 설정할 수 있는 광원
Spot light
이는 환경의 어딘가에 위치한 광원이다.
- 모든 방향으로 광선을 쏘지 않고, 특정 방향으로만 씀.
- 결과적으로 spotlight 방향의 특정 반지름 내부에 있는 오브젝트만 밝아지고 나머지는 어두워짐.
OpenGL의 spotlight는
world-space에서의 위치
,방향
,cutoff 각도
로 나타낸다.- 각 fragment에 대해 spotlight의 cutoff방향 사이(원뿔 내부)에 있는지 계산한다.
- 그리고 사이에 있으면 밝힌다.(아래 그림 참고)
LightDir
: frag에서 광원까지의 방향 벡터SpotDir
: spotlight가 겨누고 있는 방향 벡터Phi ϕ:
: spotlight의 반지름 지정하는 cutoff각도, 이 각 외부에 있는 모든것들은 빛을 받지 못함.Theta θ
: LightDir 벡터와 SpotDir 벡터 사이의 각도. spotlight의 내부에 있기 때문에 이는Phi
값보다 작아야함.
계산에서 먼저,
LightDir
벡터와SpotDir
벡터를 내적하여 이를 cutoff 각 과 비교해야한다.
Flash Light
Flashlight는 viewer의 위치에 있고, 플레이어의 관점을 향해 똑바로 겨누고 있는 spotlight이다.
- 위치와 방향이 플레이어의 위치와 방향에 따라 계속해서 업데이트된다.
이를 위해 필요한 값들은 spotlight의 위치 벡터(빛 방향 계산용), 방향 벡터, cutoff 각도이다.
1
2
3
4
5
6
struct Light {
vec3 position;
vec3 direction;
float cutOff;
//...
};
- 그다음 코드에서 값을 넘겨주자
1
2
3
lightingShader.setVec3("light.position", camera.Position);
lightingShader.setVec3("light.direction", camera.Front);
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
Cutoff 값에서 cos 값으로 넘겨준것을 주의해야한다.
LightDir
와spotDir
벡터의 내적을 계산, 내적은 각이 아닌 cos 값을 반환하므로.- cos값끼리 직접 비교하기 위함.
역연산을 통해 각을 구하는것은 비싼 연산이다. 그래서 cos값으로 전달함.
남은 것은
theta θ
값을 계산하고, 이를cutoff
값과 비교하여 spotlight의 내부에 있는지 판단한다.
1
2
3
4
5
6
7
8
9
10
11
12
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 벡터와 부호를 바꾼 direction 벡터의 내적을 계산한다.
- 부호 바꾼이유: 광원을 향하는 벡터가 필요하기 때문.
- 주의: 관련 벡터들이 정규화되어 있어야한다.
cosine 값으로 비교하므로 theta가 cutoff값보다 커야한다. 위에서 설정한 각도가 12.5 이므로 0~12.5 의 범위 안에 들어와야한다. 아래 코사인 그래프를 보면, 이는 0.9979 와 1.0 내부에 들어와야 하기 때문.
이제 다음과 같은 결과가 나온다.
- spotlight의 외곽선을 좀 더 부드럽게 해야한다.
- spotlight는 외곽선에서 점차적으로 빛을 감소시킨다.
Smooth/Soft edges
부드러운 외곽선을 가진 spotlight를 생성하기 위해, inner 원뿔과 outer 원뿔을 가지도록 해야한다.
- inner: 앞서 정의한 원뿔
- outer: 좀 더 큰 원뿔로, 점점 빛이 어두워지는 원뿔
outer 원뿔 생성
- spotlight의 방향 벡터와 외부 원뿔의 벡터(반지름)사이의 각에 대한 cosine 값을 정의하면된다.
fragment가 inner과 outer 사이에 있으면 빛의 세기 값을
0.0
~1.0
사이로 계산한다.- inner에 있으면 빛의 세기는
1.0
- outer 밖에 있으면,
0.0
- inner에 있으면 빛의 세기는
다음 공식을 사용하여
빛의 세기
를 계산할 수 있다.- ϵ (epsilon): inner (ϕ)원뿔과 outer (γ) 원뿔의 차이 (ϵ=ϕ−γ)
- 최종 결과인 I 값은 현재 fragment의 spotlight 빛의 세기이다.
- 이 공식에 관한 표는 다음과 같다.
θ | θ in degrees | ϕ (inner cutoff) | ϕ in degrees | γ (outer cutoff) | γ in degrees | ϵ | 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 |
위 식에서 볼 수 있듯이, inner cosine outer cosine 사이를 theta 값을 기반으로 보간하고 있다.
이제 우리는
빛의 세기
값을 가지고 있지만, 이 값은 spotlight outer에 있으면 음수 값을 가지고 inner에 있을 때1.0
보다 큰 값을 가지게 된다.- 이 값을 적절하게 고정시킨다면 fragment shader에 if문을 제거할 수 있다.
- 그후, 계산된 세기 값을 light 컴포넌트들에 곱해야함.
1
2
3
4
5
6
7
8
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;
...
- 여기에서는 보여주지는 않았지만, outerCutOff 값을 추가하고 uniform 값을 설정하고,inner cutoff를
12.5
, outer cutoff을17.5
로 설정했다.
- 이러한 flashlight/spotlight 타입의 램프는 공포게임에 사용하기 좋다.
- directional light와 point light를 조합하면 환하게 만들 수있다.