5 분 소요

Light

게임에서 빛의 종류

Image

  • Directional Light (방향광)
    ‘태양빛’ 같이 ‘방향’만 있으며
    거리에 따른 ‘감쇠’가 없음
    (보통 LightDir 로 처리)
  • Point Light(점광원)
    ‘전구’처럼 한 점에서 모든 방향으로 퍼짐
    거리 감쇠(Attenuation) 구현 필요
    (L = normalize(LightPos - FragPos));
  • Spot Light(스포트라이트)
    원뿔 모양으로 퍼지는 빛(손전등)
    각도와 거리 감쇠를 함께 계산
  • Ambient Light(환경광)
    전체적으로 약간 밟게 유지하는 균일한 빛
    직접광이 없어도 완전히 까맣지 않게
    (Phong 모델에선 상수 값으로 덮어씌워 구현)
    (PBR 등에선 시뮬레이션 등을 통해 실제로 환경광을 계산)

광원 모델

  • Lambert (램버트 조명)
    Diffuse를 구할때 많이 사용
    I=kd​⋅(N⋅L)⋅IL​
    • I : 최종 밝기
    • Kd : Diffuse의 반사율 (재질 고유 색상, Albedo)
    • N : 법선 벡터(Normal)
    • L : 광원 방향 벡터(Light Direction, 정규화됨)
    • (N⋅L) : 내적을 통한 입사각 효과 (빛이 직각일수록 많이 받으며, 측면일수록 약해짐)
    • IL : 광원 자체의 세기(Intensity of Light Source)
  • Phong Reflection Model
    고전적인 조명 공식
    I=ka​Ia​+kd​(N⋅L)IL​+ks​(R⋅V)αIL​
    • I : 최종 밝기
    • Ka : Ambient 반사율 (재질이 환경광을 반사하는 정도)
    • Ia : Ambient Light 강도(환경광 세기)
    • Kd : Diffuse의 반사율 (재질 고유 색상, Albedo)
    • N : 법선 벡터(Normal)
    • L : 광원 방향 벡터(Light Direction, 정규화됨)
    • (N⋅L) : 내적을 통한 입사각 효과 (빛이 직각일수록 많이 받으며, 측면일수록 약해짐)
    • IL : 광원 자체의 세기(Intensity of Light Source)
    • Ks : Specular 반사율 (얼마나 빛을 잘 반사하는지, 0이면 없음)
    • R : 반사 벡터(빛이 표면에서 반사된 방향)
    • V : 뷰어(카메라) 방향 벡터
    • ​(R⋅V)α : 스페큘러 강도 (α : 하이라이트의 날카로움 -> Shininess)
  • Blinn-Phong
    Phong의 개선 버전
    Half Vector를 이용
    연산량 절감 + 더 자연스러운 하이라이트
    I=ka​Ia​+kd​(N⋅L)IL​+ks​(N⋅H)αIL​
    • H : HalfVector(광원방향 L과 뷰어 방향 V의 중간 벡터)
      • H=L+V​ / ∣L+V∣
    • 나머지는 Phong과 동일
    • R⋅V 대신, N⋅H 를 이용N⋅H
  • PBR(Phsically Based Rendering)
    물리 기반 모델, 현대 게임 그래픽스 표준(Unreal 등)
    fr​(L,V)= D(h)⋅F(V,h)⋅G(N,V,L)​ / 4(N⋅V)(N⋅L)
    • fr : BRDF (Bidirectional Reflectance Distribution Function)
    • D(h) : Normal Distribution Function (NDF, 거칠기 → Roughness)
    • F(V,h) : Fresnel Term (시선 각도에 따른 반사율 변화)
    • G(N,V,L) : Geometry Term (마이크로 셰도잉, 빛이 미세 표면에서 가려지는 정도)
    • h : half vector
    • 𝑁⋅𝑉,𝑁⋅𝐿 : 입사/출사 각도의 영향

감쇠

Image

거리 감쇠로서
시작점 -> 거리 D까지의 선형보간

  • fallOffStart 까지는 1의 가중치(감쇠 x)
  • fallOffEnd를 넘어가면 0의 가중치(보이지 않음)
  • 거리가 같더라도 fallOffEnd가 더 긴쪽이 더 밟게 보인다

SpotLight 구현은
‘각도 감쇠’또한 필요하다
(내각,외각에 따라 원뿔 가장자리 처리가 달라진다 함)

PBR 제외 예시코드(HLSL)

float CalcAttenuation(float d, float falloffStart, float falloffEnd)
{
    // Linear falloff
    return saturate((falloffEnd - d) / (falloffEnd - falloffStart));
}

float3 BlinnPhong(float3 lightStrength, float3 lightVec, float3 normal,
                   float3 toEye, Material mat)
{
    float3 halfway = normalize(toEye + lightVec);
    float3 specular = mat.specular * pow(max(dot(halfway, normal), 0.0), mat.shininess);
    
    return mat.ambient + (mat.diffuse + specular) * lightStrength;
}

float3 ComputeDirectionalLight(Light L, Material mat, float3 normal,
                                float3 toEye)
{
    float3 lightVec = -L.direction;
    float ndotl = max(dot(lightVec, normal), 0.0);
    float3 lightStrength = L.strength * ndotl;
    
    return BlinnPhong(lightStrength,lightVec,normal,toEye,mat);
}

float3 ComputePointLight(Light L, Material mat, float3 pos, float3 normal,
                          float3 toEye)
{
    float3 lightVec = L.position - pos;

    // 쉐이딩할 지점부터 조명까지의 거리 계산
    float d = length(lightVec);

    // 너무 멀면 조명이 적용되지 않음
    if (d > L.fallOffEnd)
    {
        return float3(0.0, 0.0, 0.0);
    }
    else
    {
        lightVec /= d;
        float ndotl = max(dot(lightVec, normal), 0.0);
        
        float3 lightStrength = L.strength * ndotl;
        float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
        lightStrength *= att;
        
        return BlinnPhong(lightStrength,lightVec,normal,toEye,mat);
    }
}

float3 ComputeSpotLight(Light L, Material mat, float3 pos, float3 normal,
                         float3 toEye)
{
    float3 lightVec = L.position - pos;

    // 쉐이딩할 지점부터 조명까지의 거리 계산
    float d = length(lightVec);

    // 너무 멀면 조명이 적용되지 않음
    if (d > L.fallOffEnd)
    {
        return float3(0.0f, 0.0f, 0.0f);
    }
    else
    {
        lightVec /= d;
        float ndotl = max(dot(lightVec, normal), 0.0);
        
        float3 lightStrength = L.strength * ndotl;
        float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
        lightStrength *= att;
        
        float spotFactor = pow(max(-dot(lightVec, L.direction), 0.0), L.spotPower);
        lightStrength *= spotFactor;
        
        return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
    }
    
    // if에 else가 없을 경우 경고 발생
    // warning X4000: use of potentially uninitialized variable
}

hlsli 확장자

일반적으로 HLSL Include의 줄임말
보통 쉐이더 코드에서 공용으로 사용하는 상수,함수, 구조체 등을
모아두는 ‘헤더 파일’ 개념

  • 개발자들이 관례적으로 만든 확장자
    (C++의 hpp 같이 ‘표준 확장자’는 아님)

  • 사용처?
    공용 상수 버퍼 정의, 반복 사용 함수, 구조체 정의 등을 포함시킨다
  • Item Type(항목 형식)을
    Does not participate in build (빌드에 참여 안함)
    으로 설정해야 한다
    (아니면 ‘메인 함수’를 찾을 수 없다는 에러 발생)
    (그렇지만 쉐이더가 아니므로, main 함수 같은 진입점이 있으면 안됨)

예시 파일

// 쉐이더에서 include할 내용들은 .hlsli 파일에 작성
// Properties -> Item Type: Does not participate in build으로 설정

#define MAX_LIGHTS 3 // 쉐이더에서도 #define 사용 가능
#define NUM_DIR_LIGHTS 1
#define NUM_POINT_LIGHTS 1
#define NUM_SPOT_LIGHTS 1

// 재질
struct Material
{
    float3 ambient;
    float shininess;
    float3 diffuse;
    float dummy1; // 16 bytes 맞춰주기 위해 추가
    float3 specular;
    float dummy2;
};

// 조명
struct Light
{
    float3 strength;
    float fallOffStart;
    float3 direction;
    float fallOffEnd;
    float3 position;
    float spotPower;
};

float CalcAttenuation(float d, float falloffStart, float falloffEnd)
{
    // Linear falloff
    return saturate((falloffEnd - d) / (falloffEnd - falloffStart));
}

...

struct VertexShaderInput
{
    float3 posModel : POSITION; //모델 좌표계의 위치 position
    float3 normalModel : NORMAL; // 모델 좌표계의 normal    
    float2 texcoord : TEXCOORD0; // <- 다음 예제에서 사용
    
    // float3 color : COLOR0; <- 불필요 (쉐이딩)
};

struct PixelShaderInput
{
    float4 posProj : SV_POSITION; // Screen position
    float3 posWorld : POSITION; // World position (조명 계산에 사용)
    float3 normalWorld : NORMAL;
    float2 texcoord : TEXCOORD;
    
    // float3 color : COLOR; <- 불필요 (쉐이딩)
};

실제 쉐이더 파일에서 사용할때는

#include "Common.hlsli" // 쉐이더에서도 include 사용 가능

이런식으로 사용한다

[unroll] 키워드?

일종의 ‘속성’(Attribute)이며
GPU 쉐이더 컴파일러가 for문을 최적화 할때
컴파일러에게 ‘반복문’을 풀어
하드코딩하라고 조언하는 키워드

‘상수’의 개수를 가지는 루프 문일때
더 효과적(오버헤드 x)
(물론 사용하지 않더라도 컴파일러가 알아서
최적화할수도 있음)

HLSL for문 관련

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 toEye = normalize(eyeWorld - input.posWorld);

    float3 color = float3(0.0, 0.0, 0.0);
    
    int i = 0;
    
    // https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-for
    // https://forum.unity.com/threads/what-are-unroll-and-loop-when-to-use-them.1283096/
    
    [unroll] // warning X3557: loop only executes for 1 iteration(s), forcing loop to unroll
    for (i = 0; i < NUM_DIR_LIGHTS; ++i)
    {
        color += ComputeDirectionalLight(lights[i], material, input.normalWorld, toEye);
    }
    
    // 컴파일러에게 하드 코딩을 하도록 권장
    // 반복문을 풀어헤치도록 만들어버림
    // 개수가 '상수'라면 효과적
    // 이런걸 Attribute라 함
    [unroll]
    for (i = NUM_DIR_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; ++i)
    {
        color += ComputePointLight(lights[i], material, input.posWorld, input.normalWorld, toEye);
    }
    [unroll]
    for (i = NUM_DIR_LIGHTS + NUM_POINT_LIGHTS; i < NUM_DIR_LIGHTS + NUM_POINT_LIGHTS + NUM_SPOT_LIGHTS; ++i)
    {
        color += ComputeSpotLight(lights[i], material, input.posWorld, input.normalWorld, toEye);
    }
    return useTexture ? float4(color, 1.0) * g_texture0.Sample(g_sampler, input.texcoord) : float4(color, 1.0);
}

댓글남기기