[DX11] 렌더러 만들기

윤정민·2026년 3월 2일

Graphics

목록 보기
25/26

개요

사실 렌더러는 퐁쉐이딩만 하도록 대충 만들어두려고 했는데 보이는게 안이쁘니깐 애니메이션 작업도 흥미가 안생기더라... 그래서 PBR을 구현해보기로 했다. 인터넷에 자료가 많아서 많이 따라갔다.

개념

PBR (Physically Based Rendering)

1. PBR 개요

PBR은 물리 기반(혹은 물리 근사)으로 빛-재질 상호작용을 모델링해, 환경/조명 변화에 대해 일관된 결과를 얻는 렌더링 방식이다.
실시간 PBR에서 핵심 구성은 보통 아래 3가지로 설명한다.

  • Microfacet Model: 표면을 “아주 작은 거울 조각”들의 집합으로 보고 반사를 모델링
  • Energy Conservation: 반사 + 확산으로 나가는 에너지가 들어오는 에너지를 초과하지 않도록 제한
  • BRDF: 입사/출사 방향에 따른 반사 비율을 정의하는 함수(특히 Cook-Torrance)

2. Microfacet Model (미세면 모델)

개념
표면은 완전히 매끈하지 않고, 미세한 면(거울 조각)들이 다양한 방향을 가진다고 가정한다.

  • Normal Map: 픽셀마다 microfacet의 “평균 법선 방향”을 바꾼다.
  • Roughness: microfacet 법선 분포의 폭(퍼짐)을 바꾼다.
    • roughness ↓ : 하이라이트가 작고 날카로움
    • roughness ↑ : 하이라이트가 넓고 흐림

픽셀 법선(N) 구성 흐름
실무적으로는 “기본 법선(지오메트리)” 위에 “노멀맵(탄젠트 공간)”을 적용해 최종 월드 법선을 만든다.

1) 버텍스 노멀 보간으로 픽셀 노멀 얻기

  • VS에서 월드 공간 normal을 넘기면 PS에서 보간된 픽셀 normal을 얻을 수 있음.

2) 노멀맵을 샘플링해 TBN으로 월드 공간 변환

  • 노멀맵은 보통 Tangent Space 기준
  • 샘플한 노멀(nTS)을 TBN으로 변환해 월드 노멀로 만든다.
  • 이때 TBN의 N은 1)에서 얻은 보간 노멀을 사용
float3 ApplyNormalMap(float2 uv, float3 N, float3 T, float3 B)
{
    float3 nTS = DecodeNormalTS(gTexNormal.Sample(CommonSampler, uv).xyz);
    float3x3 TBN = float3x3(normalize(T), normalize(B), normalize(N));
    return normalize(mul(nTS, TBN)); // tangent->world
}

3) D(NDF): Roughness로 분포 폭 제어 (GGX)
Cook-Torrance 스페큘러의 첫 요소는 D (Normal Distribution Function).
여기서 roughness가 하이라이트의 모양/크기에 가장 크게 영향을 준다.

  • 사용 기법: DistributionGGX
  • NdotH: N과 H(half vector)의 내적
  • roughness ↑ → 분포가 넓어짐 → 하이라이트가 퍼짐
// D: GGX(Trowbridge-Reitz) Normal Distribution Function
// - microfacet들의 법선이 하프벡터 H 방향으로 얼마나 모여있는지(분포)를 근사
// - roughness ↓  -> 분포가 좁아짐(하이라이트 작고 날카로움)
// - roughness ↑  -> 분포가 넓어짐(하이라이트 넓고 흐림)
float DistributionGGX(float NdotH, float roughness)
{
    // UE4 관례: roughness를 제곱해서 'a'로 사용 (반응을 더 자연스럽게 만듦)
    // a가 작을수록 매우 매끈한 표면에 해당
    float a  = roughness * roughness;
    float a2 = a * a;

    // GGX 분모: ((N·H)^2 (a^2 - 1) + 1)^2
    // NdotH가 1(=H가 N과 거의 같은 방향)일 때 D가 크게 나와 하이라이트가 생김
    float denom = (NdotH * NdotH) * (a2 - 1.0f) + 1.0f;

    // 정규화까지 포함한 GGX NDF
    return a2 / max(PI * denom * denom, 1e-6);
}

3. Energy Conservation (에너지 보존)

개념
표면에서 나가는 빛(반사 + 확산)은 들어오는 빛을 넘을 수 없다.
Cook-Torrance는 보통 Specular 비율(kS)Diffuse 비율(kD)로 나눠 이를 만족시킨다.

  • kS: 반사(specular)로 나가는 비율
  • kD: 확산(diffuse)로 나가는 비율
  • 보통 트레이드오프: kD = 1 - kS
  • Metallic workflow에서는 금속일수록 diffuse가 사라짐:
    • metallic = 1 → diffuse 거의 0
    • metallic = 0 → dielectric → diffuse 유지

Fresnel: kS(반사 비율)를 각도로 결정
Fresnel은 시선/입사 각도에 따라 반사가 증가하는 현상.
실시간에서는 보통 Schlick 근사를 사용한다.

  • F0: 정면(수직)에서의 기본 반사율
    • 비금속: 대략 0.04 (4%)
    • 금속: 반사 자체가 색을 띠므로 baseColor가 F0 역할을 함
  • metalness 워크플로우의 F0:
    • F0 = lerp(0.04, baseColor, metallic)
  • Schlick:
    • F = F0 + (1 - F0) * (1 - cosTheta)^5
    • cosTheta는 보통 dot(V, H) 계열 사용
float NdotL = saturate(dot(N, L));
float NdotV = saturate(dot(N, V));
float NdotH = saturate(dot(N, H));
float VdotH = saturate(dot(V, H));

float3 F0 = lerp(float3(0.04, 0.04, 0.04), baseColor, metallic);

float3 F = FresnelSchlick(VdotH, F0);

float3 kS = F;
float3 kD = (1.0f - kS) * (1.0f - metallic);

4. BRDF (Bidirectional Reflectance Distribution Function)

개념
BRDF는 입사 방향(wi)와 출사 방향(wo)에 따라 반사되는 빛의 비율을 반환하는 함수다.
Cook-Torrance BRDF는 보통 다음처럼 구성한다.

  • Diffuse(확산): Lambert
    • diffuse = baseColor / PI
    • 에너지 보존을 위해 kD를 곱해 사용
  • Specular(반사): Cook-Torrance
    • pec = (D * G * F) / (4 * NdotV * NdotL)
    • D: microfacet 분포 (GGX)
    • G: microfacet 가림(Geometry)
    • F: Fresnel (Schlick)

Geometry(G): microfacet 가림(Shadowing/Masking)
거칠수록 미세 요철이 서로를 더 가려서 스페큘러 기여가 줄어든다.

  • View 방향에서 가림: Masking
  • Light 방향에서 가림: Shadowing
  • Smith 형태로 결합:
    • G(N,V,L) = G1(N,V) * G1(N,L)
float GeometrySchlickGGX(float NdotV, float roughness) // NdotL이 들어올 수 있음
{
    float r = roughness + 1.0f;
    float k = (r * r) / 8.0f; // direct lighting용
    return NdotV / max(NdotV * (1.0f - k) + k, 1e-6);
}
// G1: Schlick-GGX (direct lighting용 k 사용)
float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = roughness + 1.0f;
    float k = (r * r) / 8.0f; // direct lighting용
    return NdotV / max(NdotV * (1.0f - k) + k, 1e-6);
}

// Smith: view 방향 가림 * light 방향 가림
float GeometrySmith(float NdotV, float NdotL, float roughness)
{
    float ggxV = GeometrySchlickGGX(NdotV, roughness);
    float ggxL = GeometrySchlickGGX(NdotL, roughness);
    return ggxV * ggxL;
}

// Cook-Torrance specular BRDF 조립은 EvaluatePBR_Direct 내부에서:
// spec = (D*G*F) / (4*NdotV*NdotL)
// diffuse = baseColor/PI (에너지 보존 위해 kD 곱)
// 최종: (diffuse + spec) * radiance * NdotL

작업 결과

스켈레탈 메시

스태틱 메시

profile
그냥 하자

0개의 댓글