surkim 2025. 5. 2. 10:08

.pbrt 파일내 blackbody L로 지정된 광원에서

파서에서 파싱된 결과가 조단위 이상으로 매우 크게 출력 되었었습니다.

[bathroom.pbrt]
AttributeBegin
    AreaLightSource "diffuse" "blackbody L" [6500 100 ] #"color L" [2500 2500 2500]
    Shape "plymesh" "string filename" "geometry/mesh_00062.ply" 
AttributeEnd

[파싱 결과]
4400002063400960.0 4162397761699840.0 4331947501289472.0

 

초기에는 해당 값을 파서의 잘못된 계산, 보정치가 잘못 들어간 줄 알고 수치 보정을 적용했었습니다.

float Y_norm = xyz[1];
if (Y_norm > 0.0f) {
   xyz[0] /= Y_norm;
   xyz[1] = 1.0f;
   xyz[2] /= Y_norm;
 }

 

그러나 이후 이 값이 단위 문제일 수 있다는 피드백을 받고

계산 과정과 물리 단위의 의미를 검토한 결과

이 수치는 Planck 복사 법칙 기반의 정확한 물리량임을 확인하였습니다.

 

초기 제가 보정식을 만들 때는 Radiance의 Y 성분으로 정규화한 뒤 L을 곱하는 방식이었는데
이 과정에서는 Radiance [W / (sr · m²)]  단위 내 포함되어야 할 광원 면적 요소가 누락되었기 때문에
색상은 유지되더라도, 광원의 실제 면적에 따라 달라져야 할 상대 에너지 비율이 깨지는 문제가 발생했습니다.

 

보정치를 면적을 포함한 복사휘도의 절대량을 고려한 방식으로 다시 계산하거나

값 자체를 그대로 쓰고 후처리로 tone mapping 및 자동 exposure 보정을 통해 바꾸는 방식 중에

후자를 선택하여 보정하려고 합니다.


.pbrt 파일에서는 plastic, substrate, uber 등의 재질이 디퓨즈 + 스펙큘러 구성의 BRDF 혼합 모델을 따르고 있습니다.
초기 구현에서는 이들 재질에 대해

  1. 샘플링 방향은 임의의 확률 (5:5) 로 diffuse/specular를 선택했고,
  2. BRDF f와 pdf 또한 5대 5 가중치로 단순 평균 했었습니다.

이제는 Fresnel 기반으로 샘플 선택으로 조정했습니다.

샘플 분기 시 rand < F_spec 조건을 사용해,
Fresnel 계수 F = Schlick(V⋅H, F₀)에 따라 디퓨즈/스펙큘러 중 하나를 선택했습니다.

 

Throughput 계산 시 pdf 보정

선택된 경로의 pdf 뿐만 아니라

전체 혼합 분포의 pdf = pdf_diff + pdf_spec 를 통해

f / pdf_total 구조를 유지하였습니다.

 

Ks를 그대로 Fresnel F₀로 사용했던 초기 구조를 수정하고,

굴절률 η 기반 F₀ = ((η−1)/(η+1))²으로 계산한 뒤 Ks를 곱해 최종 F₀를 구성합니다.

 

이러한 구조 개선을 통해:

  • 반사 세기, 샘플링 비율, throughput 계산 간의 일관성 확보
  • 특히 우버 재질의 바닥 반사량이 비정상적으로 커지던 문제 해결했습니다.

 

  • 샘플 누적 시 어두워지는 문제는 BRDF 문제가 아니라 이미지 포맷 설정 문제였습니다.
    → VK_FORMAT_R16G16B16A16_SFLOAT 포맷에서 float 누적 시 오버플로우 발생 → 밝기가 비정상적으로 감소
    → 포맷을 R32G32B32A32_SFLOAT로 교체 후 해결
  • NaN 값 문제도 동일 원인으로 확인됩니다. 
    → 감쇠된 throughput 또는 누적된 조도 값이 float 범위/정밀도를 벗어나면서 INF/NaN 발생했습니다.

 

이전 이미지
현재 fresnel기반 샘플 선택, 이미지 포맷 교체 후 이미지 (광원 수정 X)

 

레퍼런스

 

밑은 blackbody 관련 내용 정리입니다.

 

Radiance

단위: W / (sr · m²)
단위 면적당 단위 입체각 방향으로 방출되는 방사 에너지량

 

 

Planck 복사 법칙은 특정 온도 T를 갖는 흑체(blackbody)가

파장 λ에서 얼마만큼의 에너지를 방출하는지를 계산하는 물리 법칙입니다.

 

여기서 λ(람다)는 파장이며,

보통 400nm ~ 700nm의 가시광선 범위를 일정 간격으로 샘플링하여 사용합니다.

 

이 샘플된 각 λ에 대해 Planck 수식을 적용해 스펙트럼 Radiance를 계산하고

해당 값에 CIE XYZ 민감도 곡선을 곱하여 적분함으로써 색 정보를 포함한 XYZ Radiance를 구합니다.

 

샘플 수 및 Y 정규화 계수에 따른 보정 계수를 적용합니다.

여기서 CIE Y함수 전체적분값을 미리 계산해 놓은 것으로 사용한다 합니다. 

그 후 XYZ를 RGB로 선형 변환하고, 최종적으로 사용자 정의 밝기 스케일을 곱하여 최종 RGB Radiance가 완성됩니다.

 

정리하면

 

  1. Planck 복사 법칙으로 λ별 Spectral Radiance 계산
  2. 각 λ에 대해 CIE x̄(λ), ȳ(λ), z̄(λ) 민감도 곡선을 곱하고 수치 적분
  3. 샘플 수 및 Y 정규화 계수에 따른 보정 계수 적용
  4. XYZ → RGB 선형 변환
  5. 마지막으로 밝기 스케일 L을 곱함

 

밑은 파서의 blackbody_to_rgb 함수입니다.

책에 나온 공식 그대로를 사용하고 틀린 건 저였습니다.

// 입력 예시) blackbody[0] = 2700, blackbody[1] = 10 [2700, 10]
// 출력 xyz[3]
 static void blackbody_to_xyz(const float blackbody[2], float xyz[3])
  {
    const float c = 299792458.0f;
    const float h = 6.62606957e-34f;
    const float kb = 1.3806488e-23f;

    float t = blackbody[0]; // temperature in Kelvin

    xyz[0] = 0.0f;
    xyz[1] = 0.0f;
    xyz[2] = 0.0f;
    for (int i = 0; i < nSpectralSamples; i++) {
      float wl = lerp(float(i) / float(nSpectralSamples), sampledLambdaStart, sampledLambdaEnd);

      // Calculate `Le`, the amount of light emitted at wavelength = `wl`.
      // `wl` is deliberately chose to match the wavelengths at which the X, Y
      // and Z curves are sampled.
      float l = wl * 1e-9f;
      float lambda5 = (l * l) * (l * l) * l;
      float Le = (2.0f * h * c * c) / (lambda5 * (std::exp((h * c) / (l * kb  * t)) - 1));

      xyz[0] += X[i] * Le;
      xyz[1] += Y[i] * Le;
      xyz[2] += Z[i] * Le;
    }

    float scale = float(sampledLambdaEnd - sampledLambdaStart) / float(CIE_Y_integral * nSpectralSamples);
    xyz[0] *= scale;
    xyz[1] *= scale;
    xyz[2] *= scale;

	// 여기서 자체적으로 보정을 적용했었습니다.
    // float Y_norm = xyz[1];
    // if (Y_norm > 0.0f) {
    //   xyz[0] /= Y_norm;
    //   xyz[1] = 1.0f;
    //   xyz[2] /= Y_norm;
    // }
    // else {
    //   xyz[0] = 0.0f;
    //   xyz[1] = 0.0f;
    //   xyz[2] = 0.0f;
    // }

    float L = blackbody[1];
    xyz[0] *= L;
    xyz[1] *= L;
    xyz[2] *= L;
  }

static void blackbody_to_rgb(const float blackbody[2], float rgb[3])
  {
    float xyz[3];
    blackbody_to_xyz(blackbody, xyz);
    xyz_to_rgb(xyz, rgb);
  }