Programming/컴퓨터그래픽스 (DX 11)

[Lighting] 퐁 쉐이딩(Phong Shading)과 블린퐁 쉐이딩(Blinn-Phong Shading

양양줘 2025. 10. 10. 18:59

3D 그래픽스에서 조명 모델은 시간의 흐름에 따라 진화해왔다.

1970년대 Phong 박사가

표면의 부드러운 하이라이트를 표현하는

퐁 리플렉션 모델을 처음 제안하였으며,  

1990년대 이후 실시간 3D 가속기의 발전과 함께

퐁과 블린-퐁 기반의 쉐이딩이 게임 엔진에서 사실상 표준화되었다.

 

이후 2010년 전후에는 현실적 물리 기반 표현을 목표로

PBR(Physically Based Rendering) 개념이 등장하였으며

게임 엔진의 기본 머티리얼도 이 PBR이다.

 

하지만 모바일, 인디, 레거시 엔진에서는

여전히 블린퐁이나 램버트같은

단순한 조명 모델을 사용하는 경우도 있다.

 

오늘 정리할 내용은 바로

퐁/ 블린퐁 쉐이딩이다!

 

 

퐁 쉐이딩 (Phong Shading)


 

퐁 쉐이딩(Phong Shading)은 3D 그래픽스에서 물체 표면의 조명과 색상을 현실적으로 표현하는 기법으로, 부드러운 하이라이트와 실감나는 광택을 표현한다. 최종 밝기는 ‘환경광(Ambient) + 확산광(Diffuse) + 정반사광(Specular)’의 합으로 도출하며 GPU 성능지 좋지 않았던 과거의 레거시 쉐이딩 모델이다.

 

 

퐁 쉐이딩에서는 재질의 특성을 고려하지 않고 정반사와 난반사가 동시에 일어난다고 가정한다. PBR에서는 광원이 표면에서 반사되는 것이 아니라 재질 내부로 들어간 빛이 여러번 산란되어 다시 나오는 것이다.

 

1. 환경광 (Ambient)
𝑎𝑚𝑏𝑖𝑒𝑛𝑡 = 𝑘𝑎 𝐼𝑎
환경광 = 환경광 반사 계수 * 간접광 세기

 

간접광을 포함하는 전역 조명(Global Illumination)을 간단하게 표현한 방식이다. 퐁쉐이딩에서는 전역 조명(Globl Illumination)에서 표현되어야 하는 간접광(Indirect)를 간단하게 표현하여 씬 전체에 균일하게 뿌려지는 최소 밝기를 표현한다. 즉, 가장 어두울때도 나오는 색상을 의미한다.

 

 

2. 확산광, 난반사광 (Diffuse)
𝑑𝑖𝑓𝑓𝑢𝑠𝑒 = kd Ilmax(0, NL)
난반사광 = 난반사 계수  * 광원 세기 * (법선벡터와 광원 방향벡터의 내적)

 

광원의 방향이 표면에서 모든 방향으로 균일하게 퍼져 어디에서 보든 같은 밝기를 나타내는 빛. 빛의 밝기는 표변 법선벡터와 광원 방향백터의 내적값이다. 여기서 두 벡터는 모두 단위벡터이므로 빛의 밝기는 광원 벡터와 법선 벡터의 코사인 세타 값이다.

 

 

3. 정반사광 (Specular)
𝑆𝑝𝑒𝑐𝑢𝑙𝑎𝑟 = 𝑘𝑠  𝐼𝑙 (𝑅⋅𝑉)^𝛼
정반사광 = 정반사 계수  * 광원 세기 * (광원 반사 벡터와 시선벡터의 내적)^광택계수
R = 2(N⋅L)N−L
광원 반사 벡터 = 2 * (법선벡터와 광원 방향 벡터의 내적) * 법선벡터 - 광원 방향벡터

 

광원의 방향이 표면에서 반사되어 특정 방향으로 집중되는 빛으로 입사각과 반사각이 같으며 보는 방향에 따라 밝기가 달라지는 빛이다. 반사된 빛(Reflection)과 시선벡터(View)사이의 내적(각도)에 따라 강도가 정해지며 반사 벡터와 시선 벡터의 내적값을 거듭제곱하여 빛의 밝기를 계산한다. 여기서 두 벡터는 모두 단위벡터이므로 빛의 밝기는 코사인 세타(각도)이다.

정반사광의 밝기를 구하기 위해서는 광원이 표면에 부딪힌 후 반사되는 방향인 반사벡터(R)을 구해야 한다.

 

  • L : 광원 방향 벡터 (light direction)
  • N : 표면 법선 벡터 (surface normal)
  • V : 카메라 방향 벡터 (view direction)
  • 구하려는 벡터: R(반사 벡터, reflection vector)

💡 반사벡터 R = 2(N⋅L)N−L

  • N⋅L : 법선과 광원 방향 사이의 코사인 각도
  • 2(N⋅L)N : 법선 성분을 기준으로 광원 벡터를 반사
  • −L은 빛이 표면으로 들어오는 방향(픽셀→광원)으로 정의하므로 부호를 반대로 해서 실제 반사 방향 벡터 R생성

 

 

 

블린-퐁 쉐이딩 (Blinn-Phong Shading)


퐁 쉐이딩 모델에서 정반사광(Specular) 계산은 실시간 렌더링에서 연산 비용이 높다. 이 문제를 해결하기 위해 등장한 모델이 바로 블린-퐁 쉐이딩 모델이다. 블린퐁 쉐이딩(Blinn_Phong Shading)은 퐁쉐이딩과 마찬가지로  ‘환경광(Ambient) + 확산광(Diffuse) + 정반사광(Specular)’ 의 합으로 빛의 밝기를 구하지만 정반사를 구할때 반사 벡터(R) 대신 광원 방향 벡터와 시선벡터의 중간인 하프 벡터(Half vector)를 근사치로 사용하여 단순 내적 하나로 빛의 밝기를 구한다. 블린퐁 쉐이딩은 하이라이트가 조금 더 넓고 부드럽게 표현된다.

 

퐁 쉐이딩과 블린퐁 쉐이딩의 정반사 연산 비교

 

1. Phong의 정반사광

 : 반사벡터와 시선벡터의 일치 정도로 하이라이트 강도를 계산했으나, 반사 벡터의 연산이 비용이 크다.

 

 

2. Blinn-Phong의 정반사광

 : 법선 벡터와 하프벡터의 일치 정도로 하이라이트 강도를 계산하여 연산 효율 증가.

💡  Specular = ksIlmax(0, NH)^α

       정반사광 = 정반사 계수 * 광원 세기 * (법선벡터와 하프벡터의 내적)^광택계수

 

💡  𝐻 = (𝐿+𝑉) / (∣𝐿+𝑉∣) 

      하프 벡터 = (광원 방향 벡터 + 시선 벡터) / (광원벡터 + 시선벡터)의 길이

 

 

 

블린퐁 쉐이딩 pixelshader.hlsl
Texture2D txColorMap : register(t0);
SamplerState samLinear : register(s0);

// constant buffer
cbuffer ConstantBuffer : register(b0)
{
    matrix world;
    matrix view;
    matrix projection;
    
    float4 lightDirection;
    float4 lightColor;
    
    float indirectLight;
    float directLight;
    
    float ambientReflection;
    float diffuseReflection;
    float specularReflection;
    float shininess;
    float2 padding1;
    
    float3 cameraPos;
    float padding2;
}

struct PS_INPUT
{
    float4 pos : SV_POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD;
    float3 worldPos : WORLD_POSITION;
};

// 블린퐁 쉐이딩
float4 main(PS_INPUT input) : SV_TARGET
{
    float3 N = normalize(input.normal);
    float3 L = normalize(-lightDirection.xyz);
    float3 V = normalize(cameraPos - input.worldPos);
    float3 H = normalize(L + V);

    // 확산
    float3 ambient = indirectLight * ambientReflection * lightColor.rgb;

    // 난반사
    float diff = max(dot(N, L), 0.0f);
    float3 diffuse = directLight * diffuseReflection * diff * lightColor.rgb;
   
    // 정반사
    float spec = pow(max(dot(N, H), 0.0f), shininess);
    float3 specular = directLight * specularReflection * spec * lightColor.rgb;

    // 최종 색상
    float3 finalLight = ambient + diffuse + specular;
    float3 textureColor = txColorMap.Sample(samLinear, input.texCoord);
    
    float3 finalColor = finalLight * textureColor;
    return float4(finalColor, 1.0f);
}