Phong 조명 모델의 한계와 이를 개선하는 Blinn-Phong 셰이딩(하프웨이 벡터)을 소개하고, GLSL에서의 구현 및 두 모델의 차이를 비교한다.
URL: https://learnopengl.com/Advanced-Lighting/Advanced-Lighting
Title: LearnOpenGL - Advanced Lighting
조명 장들에서는 장면에 기본적인 현실감을 더하기 위해 Phong 조명 모델을 간단히 소개했습니다. Phong 모델은 보기 좋지만, 이 장에서 집중할 몇 가지 미묘한 점(제약)이 있습니다.
Phong 조명은 훌륭하고 매우 효율적인 조명 근사이지만, 특정 조건에서는 정반사(specular) 반사가 무너집니다. 특히 shininess(광택) 속성이 낮아 큰(거친) 정반사 영역이 생길 때 그렇습니다. 아래 이미지는 평평한 텍스처 평면에 정반사 shininess 지수로 1.0을 사용했을 때 어떤 일이 일어나는지 보여줍니다:

가장자리에서 정반사 영역이 즉시 잘려 나가는 것을 볼 수 있습니다. 이런 일이 일어나는 이유는 뷰 벡터(view)와 반사 벡터(reflection) 사이의 각도가 90도를 넘지 않기 때문입니다. 각도가 90도보다 커지면 결과적인 내적(dot product)이 음수가 되고, 그 결과 정반사 지수는 0.0이 됩니다. 아마도 “어차피 90도보다 큰 각도에서는 빛을 받지 않으니 문제가 아닐 텐데?”라고 생각할 수 있습니다.
그렇지 않습니다. 이는 확산(diffuse) 성분에만 해당합니다. 법선(normal)과 광원(light source) 사이의 각도가 90도보다 크다는 것은 광원이 조명되는 표면 아래에 있다는 뜻이므로, 그때는 빛의 확산 기여가 0.0이 되는 것이 맞습니다. 하지만 정반사 조명에서는 광원과 법선 사이의 각도를 재는 것이 아니라, 뷰 벡터와 반사 벡터 사이의 각도를 잽니다. 아래 두 이미지를 보세요:

여기서 문제가 분명해집니다. 왼쪽 이미지는 익숙한 Phong 반사를 보여주며, θ는 90도보다 작습니다. 오른쪽 이미지에서는 뷰 벡터와 반사 벡터 사이의 각 θ가 90도보다 커졌고, 그 결과 정반사 기여가 0이 되어버립니다. 일반적으로는 뷰 방향이 반사 방향과 많이 다르기 때문에 문제가 되지 않지만, 정반사 지수가 낮으면 정반사 반경이 충분히 커져 이런 조건에서도 기여가 생길 수 있습니다. 그런데 90도를 넘는 각도에서 이 기여를 0으로 만들어버리기 때문에 첫 번째 이미지에서 본 아티팩트가 발생합니다.
1977년 James F. Blinn은 지금까지 사용해 온 Phong 셰이딩을 확장한 Blinn-Phong 셰이딩 모델을 소개했습니다. Blinn-Phong 모델은 전반적으로 매우 유사하지만, 정반사 모델에 조금 다른 접근을 취함으로써 위 문제를 해결합니다. 반사 벡터에 의존하는 대신, 뷰 방향과 광원 방향의 정확히 중간에 있는 단위 벡터인 하프웨이(halfway) 벡터를 사용합니다. 이 하프웨이 벡터가 표면의 법선 벡터와 더 잘 정렬될수록 정반사 기여가 커집니다.

뷰 방향이 (이제는 가상의) 반사 벡터와 완벽히 정렬되면, 하프웨이 벡터는 법선 벡터와 완벽히 정렬됩니다. 뷰 방향이 원래의 반사 방향에 가까울수록 정반사 하이라이트가 더 강해집니다.
여기서 어떤 방향에서 관찰하더라도 하프웨이 벡터와 표면 법선 사이의 각도는 90도를 절대 넘지 않는다는 것을 알 수 있습니다(물론 광원이 표면 아래 깊이 있는 경우는 예외입니다). 결과는 Phong 반사와 약간 다르지만, 일반적으로는 더 시각적으로 그럴듯하며, 특히 정반사 지수가 낮을 때 그렇습니다. Blinn-Phong 셰이딩 모델은 또한 OpenGL의 이전 고정 기능 파이프라인에서 사용되던 정확히 그 셰이딩 모델이기도 합니다.
하프웨이 벡터를 구하는 것은 쉽습니다. 광원 방향 벡터와 뷰 벡터를 더한 뒤 정규화하면 됩니다:
이는 GLSL 코드로 다음과 같이 옮길 수 있습니다:
vec3 lightDir = normalize(lightPos - FragPos);
vec3 viewDir = normalize(viewPos - FragPos);
vec3 halfwayDir = normalize(lightDir + viewDir);
그 다음 정반사 항의 실제 계산은 표면 법선과 하프웨이 벡터의 내적을 0으로 클램프(clamp)하여 두 벡터 사이의 코사인 각을 구하고, 이를 다시 정반사 shininess 지수로 거듭제곱합니다:
float spec = pow(max(dot(normal, halfwayDir), 0.0), shininess);
vec3 specular = lightColor * spec;
이것이 Blinn-Phong의 전부입니다. Blinn-Phong과 Phong 정반사 반사의 유일한 차이는, 이제 뷰 벡터와 반사 벡터 사이의 각도 대신 법선과 하프웨이 벡터 사이의 각도를 측정한다는 점입니다.
하프웨이 벡터를 도입하면 Phong 셰이딩에서 보였던 정반사 잘림(cutoff) 문제는 더 이상 발생하지 않아야 합니다. 아래 이미지는 정반사 지수 0.5에서 두 방법의 정반사 영역을 보여줍니다:

Phong과 Blinn-Phong 셰이딩 사이의 또 다른 미묘한 차이는, 하프웨이 벡터와 표면 법선 사이의 각도가 뷰 벡터와 반사 벡터 사이의 각도보다 종종 더 작다는 점입니다. 그 결과 Phong 셰이딩과 비슷한 비주얼을 얻으려면 정반사 shininess 지수를 조금 더 높게 설정해야 합니다. 경험칙으로는 Phong shininess 지수의 2~4배 사이로 설정하는 것이 일반적입니다.
아래는 Phong 지수를 8.0으로, Blinn-Phong 성분을 32.0으로 설정했을 때 두 정반사 모델을 비교한 것입니다:

Blinn-Phong 쪽 정반사 지수가 Phong에 비해 조금 더 날카롭게 보이는 것을 확인할 수 있습니다. 이전에 Phong 셰이딩에서 얻던 결과와 유사하게 만들려면 보통 약간의 튜닝이 필요합니다. 하지만 Blinn-Phong 셰이딩은 기본 Phong 셰이딩보다 일반적으로 더 현실적이므로 그럴 가치가 있습니다.
여기서는 일반 Phong 반사와 Blinn-Phong 반사 사이를 전환하는 간단한 프래그먼트 셰이더를 사용했습니다:
void main()
{
[...]
float spec = 0.0;
if(blinn)
{
vec3 halfwayDir = normalize(lightDir + viewDir);
spec = pow(max(dot(normal, halfwayDir), 0.0), 16.0);
}
else
{
vec3 reflectDir = reflect(-lightDir, normal);
spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);
}
간단한 데모의 소스 코드는 여기에서 찾을 수 있습니다. b 키를 누르면 데모가 Phong 조명에서 Blinn-Phong 조명으로(또는 그 반대로) 전환됩니다.