개념이나 기술적으로 틀린 부분이 있을 수 있습니다. 있다면 알려주세요 ~
- 세 종류의 라이팅 구현(1) - 소개 편
https://woo-dev.tistory.com/269
마지막으로 구현해 볼 라이팅은 SpotLight이다. SpotLight는 특정한 위치에서 특정한 방향으로 원뿔(cone) 모양의 광선을 발사하는 광원이다. 주로 다음과 같은 효과를 나타내기 위해 사용되는 광원이다.
해당 광원 자체를 구현하는 데는 그렇게 많은 시간이 걸리지 않았는데, 기존의 오일러 각 회전 시스템으로 구현하다가 짐벌락을 경험했다. 그래서 쿼터니언에 대해 공부하고 바꾸는데 꽤 많은 시간이 걸렸다.. 바꾸고도 계속해서 몇 번이나 이상한 결과가 나와서 디버깅 하는 데만 며칠을 보낸 것 같다.
어쨌든.. spotlight을 구현하기 위해 먼저 이해해야 할 그림은 다음과 같다.
spotlight은 위와 같이 cone 모양의 광원을 발사하는데, cone은 inner cone과 outer cone으로 구성된다. 구현마다 다를 수 있는데 inner cone에 속하는 픽셀들은 빛의 세기를 100% 적용받을 수도 있고 아닐 수도 있다. 그리고 inner cone과 outer cone 사이의 픽셀들은 감쇠된 빛을 받는다. 그리고 outer cone 외부의 점들은 해당 spotlight의 빛을 전혀 받지 못한다. 그런데 inner와 outer cone 사이의 색이 그 밖의 영역과 확 달라지면 어색해보일 것이다.
만약 inner cone에도 감쇠를 적용한다면 outer cone에 적용하는 감쇠 공식과 동일하기 때문에 inner cone의 의미가 없어진다. 따라서 만약 모두 감쇠를 적용할 것이라면 하나의 cone만 구현하면 된다.
즉 다음과 같이 어색한 결과가 나올 수 있다. 각 영역이 너무 뚜렷하게 구분된다.
가운데의 타원이 inner cone이고 밖의 타원이 outer cone 영역이다. 위 그림은 테스트 용도로 단순히 inner cone 빛 세기의 반만 받도록 하긴 했지만 어쨌든 어색하다.
이러한 이유로 점광원(point light)과 마찬가지로 spotlight 또한 감쇠 효과가 적용되는데, point light과는 다른 감쇠 방법이 적용된다. spotlight은 inner cone에서 멀어질 수록 감쇠를 많이 받게 된다. 이러한 효과는 빛이 갑자기 나타나거나 없어지는 어색한 현상을 막아줄 수 있다. 즉 fade 효과를 주어 빛의 감쇠가 완만하게 진행되도록 하는 것이다.
아래 그림을 보면 inner cone에 해당하는 빛에서 점점 멀어질 수록 빛의 세기가 천천히 감소하여 0에 자연스럽게 수렴하는 모습을 볼 수 있다. 영역의 구분이 덜 뚜렷하고 자연스럽다.
우선 감쇠를 생각하기 전에 가장 먼저 해야할 것은 inner cone의 angle과 outer cone의 angle을 설정하여 기본적으로 cone 형태의 빛을 발사할 수 있도록 하는 것이다. 초반의 그림에서 봤듯이 spotlight는 빛의 방향을 나타내는 방향 벡터가 필요하다. 그리고 이 빛 벡터를 inner, outer cone angle 만큼 회전시키면 다음과 같이 전구로부터의 각 cone을 나타내는 방향 벡터를 얻을 수 있다.
이러한 방향 벡터들은 픽셀 셰이더에서 조명을 계산하는 데 필요하다. 사실 나는 시각화를 위해 음수각 양수각에 대한 벡터를 모두 구했는데, 양수 및 음수 각 중 하나씩만 있으면 된다. 이유는 잠시 후 살펴본다.
그리고 빛 벡터와 별개로 광원의 위치가 필요하다. 따라서 광원의 위치, 빛 벡터, inner cone 벡터, outer cone 벡터 이렇게 픽셀 셰이더로 전달한다. (빛의 색상, 세기 등은 별개로)
정점 셰이더에서 특별히 할 일은 없다. 그냥 정점 셰이더가 할 일을 하도록 하면 되고, 단 점광원과 마찬가지로 월드 공간의 정점은 꼭 픽셀 셰이더로 전달해주어야 한다.
픽셀 셰이더는 가장 우선적으로 현재 프래그먼트(픽셀)의 위치가 outer cone의 영역 안에 있는지 밖에 있는지 계산해야 한다. 영역 밖에 있으면 현재 광원의 영향을 받지 않으므로 무시하면 된다. 현재 픽셀이 각 cone에 속하는지 판단하는 방법은 간단하다.
광원으로부터 현재 픽셀까지의 벡터를 구한 뒤 이 벡터와 빛 벡터가 이루는 각이 outer cone angle보다 크면 밖에 있는 것이고 작으면 안에 있는 것이다. 각도를 비교하기 위해선 항상 cos을 이용해왔고, cos을 효율적으로 구하기 위해선 크기가 1인 두 벡터를 내적하여 구해왔다.
따라서 위와 같이 광원으로부터 현재 픽셀까지의 벡터인 CurDir을 구하고 LightDir과의 내적을 통해 cos값(Θ)을 구한다. 그리고 OutDir과 LightDir의 내적을 통해 cos값(Φ)을 구한다. cos은 각이 클 수록 값이 작아지므로 CurDir과 LightDir과의 내적 값이 더 작다면 outer cone의 밖에 있는 것이다.
여기서 잘 생각해보면 cone에 대한 벡터는 아래와 같이 빛 벡터로부터 양수각, 음수각 2개를 가질 수 있는데
cos은 성질상 각이 동일하면 음수 양수 관계없이 값이 동일하기 때문에 사실상 양수 음수각이 의미가 없다. 또한 빛 벡터와 outer cone vector 사이의 각도가 사실상 절댓값 90도를 넘을 수가 없기 때문에 (90도 자체도 말이 안되고 그건 스포트라이트라고 볼 수 없다. 언리얼에서도 대략 최대 80도까지 정도로 제한하는 듯 하다.) cos값의 부호에 대해 걱정하지 않아도 된다.
즉 양수각이든 음수각이든 상관없이 내적값을 비교하면 된다. 이제 해당 조명에 영향을 받지 않는 픽셀은 버려졌다. 다음엔 동일한 방식으로 현재 프래그먼트가 inner cone 내부에 있는지 확인한다. 만약 내부에 있을 경우 빛의 세기를 모두 받길 원한다면 감쇠없이 출력하면 된다. 사실 inner cone 내부에도 감쇠를 적용한다면 inner cone은 의미가 없다.
이제 감쇠 계산에 대해 살펴본다. 감쇠 공식은 매우 간단하며 inner cone과 outer cone에 모두 동일하게 적용할 수 있다.(inner cone 내부도 감쇠 효과를 적용하기로 했다면)
현재 프래그먼트가 받게 될 빛의 세기는 다음과 같다.
θ와 γ는 모두 angle에 대한 cos 값이다. θ는 광원으로부터 현재 픽셀을 잇는 벡터와 빛 벡터 사이의 각도에 대한 cos 값이다. γ는 outer cone 벡터와 빛 벡터 사이의 각도에 대한 cos 값이다. ϵ은 innerConeCos와 outerConeCos의 차(-)이다. inner cone과 빛 벡터 사이의 각이 항상 outer cone과 빛 벡터 사이의 각보다 작으니 결과는 항상 양수이다. (cos 값은 항상 inner가 크므로)
다음은 픽셀 셰이더의 코드이다.
float4 main(PS_INPUT input) : SV_TARGET
{
//////////////////////////
// Spot Light
//////////////////////////
// cos은 각이 양수 음수 상관없이 값이 동일하다.
// 따라서 0~90도 내에선 내적이 모두 양수값이다.
// 만약 현재 프래그먼트에 대한 벡터와의 내적이 더 작다면
// 사잇각이 더 큰 것이므로 SpotLight의 영향을 받지 않는다.
float3 fragmentDir = normalize(input.WPos - sLightPosition).xyz;
float3 WorldLightDir = normalize(sLightDir).xyz;
float fragmentCos = dot(WorldLightDir, fragmentDir);
// Negative(음수각)도 상관없음 어차피 cos 값은 같기에
float outerConeCos = dot(WorldLightDir, sOuterConeDirPositive.xyz);
// cos이 더 작다는 말은 angle이 더 크다는 것
// 즉 범위 밖에 있으므로 spotlight의 영향을 받지 않는다.
if (fragmentCos < outerConeCos)
{
return float4(0.f, 0.f, 0.f, 1.f);
}
/////////////////////////////////////////////////////////
//// 아래부터는 범위 내에 존재하는 픽셀에 대한 연산
//// 우선 Diffuse 및 Specular를 구한다.
//// 여기선 모든 cone 내부의 영역에 대해 감쇠 효과를 적용
float Diffuse = 0.f;
float Specular = 0.f;
float3 PhongD = float3(0.f, 0.f, 0.f);
float3 PhongS = float3(0.f, 0.f, 0.f);
float att = 1.f;
float3 WorldNormal = normalize(input.Normal);
// Get Diffuse
Diffuse = max(dot(WorldNormal, -WorldLightDir), 0.f);
// Get Specular
float3 RefDir = normalize(reflect(WorldLightDir, WorldNormal));
float3 ViewDir = normalize(input.WPos.xyz - CameraPosition.xyz);
Specular = max(dot(RefDir, -ViewDir), 0.f);
Specular = pow(Specular, 30.f);
// spotFactor: 광원의 방향 벡터와 광원으로부터 현재 픽셀 까지의 벡터 사이의 cos
float spotFactor = dot(sLightDir.xyz, fragmentDir);
float innerConeCos = dot(WorldLightDir, sInnerConeDirPositive.xyz);
// 받게 될 빛의 세기의 %(1-감쇠율)을 구한다.
float epsilon = innerConeCos - outerConeCos;
att = (spotFactor - outerConeCos) / epsilon;
PhongD = Diffuse * (sLightColor.rgb * input.Color);
PhongS = Specular * (sLightColor.rgb * float3(0.7f, 0.7f, 0.7f));
float3 FinalColor = att * sLightIntensity * (PhongD + PhongS);
return float4(FinalColor, 1.f);
}
이게 끝이다. 원리만 이해하면 어렵지 않은 편이다. 여기선 광원과 픽셀의 거리에 따른 감쇠 효과는 적용하지 않았는데 이는 나중에 시간이 되면 추가해본다.
마지막으로 아래는 구현한 spotlight의 테스트 영상이다.
Reference
'게임 공부 > 게임 개발 일지' 카테고리의 다른 글
앞으로 더 적용해야 할 것.. (0) | 2021.09.14 |
---|---|
오브젝트 및 라이팅 생성 및 배치 테스트 (0) | 2021.09.07 |
픽셀 셰이더의 결과의 일부분이 카메라의 위치에 따라 검은색이 나온다면.. (0) | 2021.08.28 |
오일러 각 기반 회전 시스템을 쿼터니언으로 바꾸었다. (0) | 2021.08.26 |
개발 도중 겪은 문제 혹은 버그 그리고 해결한 방법들 (0) | 2021.08.15 |
댓글