https://woo-dev.tistory.com/269
개념이나 기술적으로 틀린 부분이 있을 수 있습니다. 있다면 알려주세요 :)
가장 먼저 구현해볼 라이팅은 Directional Light이다. 이전 포스팅해서 말했듯이 Directional Light는 무한히 멀리 떨어져있다고 가정하고 평행한 광선을 쏘는 빛이다. 따라서 태양과 같은 광원을 표현하기에 적합하다.
그림으로 표현하면 다음과 같다.
광원이 빛을 쏘는 방향을 방향 벡터로 나타낼 수 있다. 그리고 이 방향 벡터를 조명을 계산할 때 빛 벡터로 사용하면 될 것 같다.
언리얼 엔진에선 x축(정면)과 일치하는 벡터가 빛의 방향이고 이를 Pitch(-46도)만큼 회전시킨 벡터의 방향이 빛의 default 방향이 된다. 즉 아래의 흰색 벡터의 방향과 같다.
DirectX에선 언리얼 엔진과 좌표축이 다르다. 정면이 z축 오른쪽이 x축이기 때문에 z축 방향 벡터를 x축 기준(Pitch) -46도 회전시켜 주면 된다.
결국 정상적이라면 각 방향 벡터가 회전한 만큼 자동으로 방향이 업데이트가 될 것이기에, 정면 벡터(Forward Vector)를 빛의 방향 벡터로 사용하면 된다.
앞으로 셰이더에서 Directional Light 조명 계산에 빛 벡터가 필요할 때 Forward Vector를 사용하면 된다. 셰이더에선 노멀라이즈된 값이 필요하기 때문에 미리 노멀라이즈된 벡터를 전달하는 것이 좋다고 생각된다.
(우선 파악한 정도로는 ) 그 외에 필요한 값으로는 빛의 강도(Intensity)와 빛의 색상(Color) 정도가 있다. 거리에 따라 받게 되는 빛의 세기가 변하는 감쇠(Attenuation) 효과는 해당 라이팅에는 적용되지 않는다. 태양의 빛의 세기가 거리에 따라 달라지는 것은 별의미가 없기 때문일 듯하다. 감쇠 효과는 나머지 두 모델에 적용된다.
언리얼 엔진에서 Directional Light의 강도는 lux라는 단위를 사용한다고 한다. lux는 조도의 단위이며 룩스 또는 럭스라고 한다. lux는 단위 면적당 루멘(lm)을 의미한다. 루멘이란 광원이 내보내는 빛의 총량을 의미한다. 이를 좀 더 이해하기 쉽게 나무위키의 예시를 가져와보면 다음과 같다.
전구의 반을 가리면 반은 빛이 보이지 않는다. 즉 광원이 내보내는 빛의 총량(루멘)이 작아진다. 또 다른 예로 전구를 대상으로부터 멀리 떨어뜨린다고 광원이 내보내는 빛의 총량(루멘)이 변하진 않지만, 단위 면적당 루멘인 lux는 작아진다. 전구로부터 떨어지면 비추는 범위는 넓어질 수 있지만, 단위 면적당 받는 빛의 양은 적어지기 때문인 것 같다.
대충 의미는 이해했지만 아직 어떻게 구현할지는 감이 잡히지 않아 이 부분은 일단 보류.. 우선은 단순히 Intensity를 0.0~1.0 범위로 하여 밝기 조정을 한다.
우선 빛과 관련된 데이터는 빛의 방향을 의미하는 방향 벡터, 빛의 색상, 빛의 강도(Intensity)가 필요하다. 또한 Specular 계산을 위해선 카메라(Eye)의 위치가 필요하다. 조명 계산에 물체 표면의 색상도 필요하다.
정점 셰이더의 입력으로는 정점, 노멀, 색상을 받는다. (텍스쳐 혹은 라이트맵을 사용한다면 색상 대신 사용하면 된다)
정점 셰이더에선 다음과 같이 각 요소를 적절한 공간으로 이동시키는 연산만 수행한다.
VS_OUTPUT main(VS_INPUT input)
{
VS_OUTPUT output;
output.Pos = mul(float4(input.Pos, 1), WorldViewProjection);
output.WPos = mul(float4(input.Pos, 1), World);
output.Color = input.Color;
output.Normal = normalize(mul(input.Normal, (float3x3) World));
return output;
}
WPos는 픽셀 셰이더에서 카메라의 좌표와 함께 월드 공간에서의 View(Camera) Vector를 만들기 위해 필요하다.
픽셀 셰이더에서는 주어진 데이터를 가지고 Diffuse와 Specular를 구하고 이를 합쳐서 최종 컬러로 출력한다.
우선 Diffuse를 구하려면 빛의 방향 벡터(LightDir)와 현재 픽셀의 노멀이 필요하다. 이들은 constant buffer로 받는다. 두 데이터의 공간은 일치해야 한다. 노멀은 정점 셰이더에서 이미 월드 공간으로 변환이 되었으므로, 다시 노멀라이즈만 한 번 더 해준다.
LightDir은 이미 회전 및 이동이 적용되어 월드 공간으로 이동된 상태로 넘어오기 때문에 월드 변환은 불필요하다. 따라서 노멀과 동일하게 노멀라이즈만 적용한다. 이제 Diffuse를 구하기 위해 LightDir와 노멀을 내적한다. 이때, LightDir과 노멀의 각을 제대로 계산할 수 있도록 방향을 맞춰주는 것이 중요하다.
Diffuse가 음수면 안되므로, max 함수를 통해 결과가 음수일 경우 0으로 설정한다. 이후 최종적으로 Diffuse에 대한 색상 값 계산을 위해 빛의 색상, 빛의 강도, 현재 프래그먼트(픽셀)의 색상을 곱한다.
// Get Diffuse
float3 WorldLightDir = normalize(LightDir.xyz);
float3 WorldNormal = normalize(input.Normal);
float Diffuse = max(dot(-WorldLightDir, WorldNormal), 0);
float3 PhongD = Diffuse * (LightColor.rgb * input.Color);
Specular는 Diffuse가 존재할 경우에만 의미가 있으므로 Diffuse가 존재할 경우에만 계산하도록 한다.
if (Diffuse > 0.f)
{
// Get Specular
}
우선 Specular는 reflection vector와 view vector 간의 내적이므로 두 벡터를 먼저 구해야 한다. 카메라(Eye)의 위치는 빛과 마찬가지로 이미 월드 행렬인 상태로 넘어온다고 가정하기 때문에 월드 변환이 필요없다. 이를 Wpos와 함께 계산하여 View vector를 만들고 노멀라이즈를 한다. Reflection vector는 입사광(LightDir)과 노멀을 이용하여 구한다.
float3 ViewDir = normalize(input.WPos.xyz - CameraPosition);
float3 ReflectionVec = normalize(reflect(LightDir.xyz, WorldNormal));
이후 Specular를 구한다. 이 또한 정상적인 각도를 계산할 수 있도록 방향을 알아서 잘 맞춘다. specular는 표면의 매끄러움의 정도에 따라 곱하게 되는 거듭 제곱의 수가 다른데, 이는 필요에 따라 알아서 설정해준다. 여기선 일단 고정값으로 30.0을 사용한다.
float Specular = max(dot(ReflectionVec, -ViewDir), 0);
Specular = pow(Specular, 30.f);
스페큘러 반사의 최종 색상엔 물체의 색상 대신 RGB값이 모두 같은 grayscale의 값을 사용한다. 즉 빛의 색상을 따라가되, 색상의 진함 연함의 정도만 결정한다.
PhongS = Specular * (LightColor.rgb * float3(0.7f, 0.7f, 0.7f));
이후 마지막으로 Diffuse와 Specular를 더해주고 빛의 intensity를 곱해주어 최종 색상을 결정하고 이를 픽셀 셰이더의 결과로 출력하면 된다.
float3 FinalColor = LightIntensity * (PhongD + PhongS);
마지막으로 픽셀 셰이더의 main 함수를 정리하면 다음과 같다.
float4 main(PS_INPUT input) : SV_TARGET
{
// Get Diffuse
float3 WorldLightDir = normalize(LightDir.xyz);
float3 WorldNormal = normalize(input.Normal);
float Diffuse = max(dot(-WorldLightDir, WorldNormal), 0);
float3 PhongD = Diffuse * (LightColor.rgb * input.Color);
// Get Specular
float3 PhongS = float3(0.f, 0.f, 0.f);
if (Diffuse > 0.f)
{
float3 ViewDir = normalize(input.WPos.xyz - CameraPosition);
float3 ReflectionVec = normalize(reflect(LightDir.xyz, WorldNormal));
float Specular = max(dot(ReflectionVec, -ViewDir), 0);
Specular = pow(Specular, 30.f);
PhongS = Specular * (LightColor.rgb * float3(0.7f, 0.7f, 0.7f));
}
float3 FinalColor = LightIntensity * (PhongD + PhongS);
return float4(FinalColor, 1.f);
}
Directional Light 테스트
매우 높은 곳에서 광선을 쏜다고 가정하기 때문에 광원의 위치는 조명에 영향을 끼치지 않는다. 다만 Rotation은 광원이 빛을 쏘는 방향(벡터)를 변화시키기 때문에 영향을 미친다.
'게임 공부 > 게임 개발 일지' 카테고리의 다른 글
개발 도중 겪은 문제 혹은 버그 그리고 해결한 방법들 (0) | 2021.08.15 |
---|---|
세 종류의 라이팅 구현 (3) - Point Light 편 (0) | 2021.08.13 |
Constant Buffer 관련 실수한 것 한 가지 | 메모리 정렬(alignment) (0) | 2021.08.07 |
세 종류의 라이팅 구현 (1) - 소개 | Directional Light, Point Light, Spot Light (0) | 2021.08.05 |
현재 라이팅 문제점 (1) (0) | 2021.07.17 |
댓글