Company
교육 철학

DIFFUSE(난반사)

개요

Ray Tracing in One Weekend 7장(Diffuse / Lambertian 난반사)를 CUDA GPU로 구현했다.
레이가 구체에 hit 하면, 표면에서 빛이 한 방향으로 튕겨 나가는 것이 아니라 법선(normal) 주변으로 랜덤하게 산란(scatter)하는 현상을 시뮬레이션한다.
GPU 구현에서 중요한 포인트는 아래와 같다.
난반사 방향을 만들기 위해 RandomInUnitSphere() 같은 랜덤 벡터 생성이 필요하다.
CPU 버전은 보통 RayColor()재귀(recursion)로 구현하지만, GPU에서는 스레드 스택이 작아 깊은 재귀가 위험하므로 루프 기반(bounce loop) 으로 변환했다.
여러 번 반사될수록 에너지가 줄어드는 attenuation(감쇠) 를 누적한다.
멀티샘플링 + 감쇠가 들어가면 화면이 어둡게 나오기 쉬워, 출력 시 감마 보정(gamma=2.0) 을 적용한다.

변경 파일 목록

파일
변경 유형
설명
수정
RandomInUnitSphere, 난반사 bounce 루프, 감마 보정 추가

1. 난반사(Diffuse) 원리

1.1 Lambertian(무광) 산란 모델

무광(matte) 표면은 빛을 특정 방향으로 ‘거울처럼’ 반사하지 않고, 표면의 미세한 요철 때문에 여러 방향으로 퍼지듯 산란된다.
이 예제에서는 아주 단순한 모델로 아래 방식을 사용한다.
hit 지점: p
법선: n
산란 목표점: target = p + n + random_in_unit_sphere()
즉, 새 레이 방향은 target - p가 된다.
입사 레이 → ●(hit) ↗ ↑ ↖ (법선 + 랜덤) 방향으로 새 레이 생성
Plain Text
복사
레이는 보통 Ray(origin, direction) 형태다. 난반사에서 새 레이는 hit 지점에서 다시 출발해야 하므로 origin = p로 둔다.
target = p + n + r (여기서 r = random_in_unit_sphere())
direction = target - p = n + r
p는 “방향”을 만들기 위한 기준점이고, 실제로 방향을 결정하는 핵심은 n + r이다.
r만 쓰면 모든 방향(구 전체)으로 균일하게 뽑히는 반면, n을 더하면 분포의 중심이 법선 방향으로 이동한다.
그 결과 n + r는 통계적으로 법선과 같은 쪽(표면 바깥쪽)으로 더 자주 향한다.
(개념) - r: 원점 주변(구 전체) 랜덤 - n + r: 중심이 n으로 이동 → 법선 쪽 반구로 “치우친” 분포
Plain Text
복사
(1) hit 지점 p와 법선 n ↑ n | | 입사 레이 → ● p ↗ ↑ ↖ (새 레이가 여러 방향으로 퍼짐) (2) r = random_in_unit_sphere() 는 '원점 주변' 랜덤 벡터 unit sphere (반지름 1) _______ / \ | • r | \_________/ (3) n을 더하면 분포 중심이 n 쪽으로 이동 r 만 쓰면: n + r 를 쓰면: (구 전체) (법선 방향으로 치우침) ↖ ↑ ↗ ↖ ↑ ↗ \|/ \ | / ← ---•--- → ← ---●--- → /|\ / | \ ↙ ↓ ↘ ↙ ↓ ↘ (아래쪽도 많이 나옴) (위쪽(법선 쪽)으로 더 자주 나옴)
Plain Text
복사
이 방식(n + random_in_unit_sphere)은 구현이 단순해서 학습용으로 좋지만, 물리 기반으로 더 “정확한” Lambertian은 법선과 이루는 각도 θ에 대해 cos(θ)로 가중된 분포(cosine-weighted hemisphere)를 따른다.
여기서는 간단히 “법선 주변으로 랜덤하게 튄다”를 흉내 내는 버전
더 정확한 샘플링은 별도의 샘플링 기법(예: cosine-weighted hemisphere sampling)을 사용한다.
드물게 n + r가 거의 0에 가까워지면(방향이 너무 작아지면) 수치적으로 불안정해질 수 있다.
그럴 때는 direction이 매우 작으면 n으로 대체하는 식의 안전장치를 둔다.

1.2 에너지 감쇠(attenuation)

반사(산란)할 때마다 에너지가 일정 비율로 줄어드는 것으로 모델링한다.
이번 구현은 “난반사 1회마다 50% 감소”로 단순화했다.
반사 횟수
감쇠율
설명
0회
1.0
직접 배경을 봄
1회
0.5
한 번 산란
2회
0.25
두 번 산란
n회
0.5^n
기하급수적으로 감소

2. GPU에서 재귀 → 루프 변환

2.1 문제: GPU 스택 오버플로우 위험

CPU 버전 튜토리얼에서는 보통 아래처럼 재귀로 작성한다.
// CPU 버전 스타일(개념) Color RayColor(const Ray& r) { if (hit) { return 0.5 * RayColor(scatteredRay); } return SkyColor(r); }
C++
복사
하지만 GPU는 스레드당 스택이 작고(환경/컴파일 옵션에 따라 다르지만 기본이 작음), virtual 호출 + 지역 변수까지 포함되면 깊은 재귀가 위험해진다.

2.2 해결: bounce 루프

재귀에서 “콜스택에 쌓이던 상태”를 아래 2개로 치환한다.
currentRay
attenuation
__device__ Color RayColor(const Ray& r, Hittable** world, curandState* localRandState) { Ray currentRay = r; Color attenuation(1.0, 1.0, 1.0); for (int bounce = 0; bounce < 50; ++bounce) { HitRecord rec; if ((*world)->Hit(currentRay, 0.001, DBL_MAX, rec)) { // 난반사: 법선 + 랜덤 벡터 Vec3 target = rec.p + rec.normal + RandomInUnitSphere(localRandState); currentRay = Ray(rec.p, target - rec.p); // 에너지 감쇠 attenuation *= 0.5; continue; } // 더 이상 hit 하지 않으면 배경색에 감쇠를 적용하고 종료 return attenuation * SkyColor(currentRay); } // 최대 bounce 초과: 충분히 어두워졌다고 보고 종료 return Color(0.0, 0.0, 0.0); }
C++
복사
CPU 재귀
GPU 루프
return 0.5 * RayColor(newRay)
attenuation *= 0.5; currentRay = newRay;
콜스택에 상태가 쌓임
변수로 상태를 유지
깊은 재귀 시 위험
안정적

3. RandomInUnitSphere: GPU 랜덤 벡터(Rejection sampling)

3.1 구현 코드(C++/CUDA C++)

#include <curand_kernel.h> __device__ Vec3 RandomInUnitSphere(curandState* localRandState) { while (true) { // [0,1] 난수 3개를 [-1,1]로 변환 Vec3 p( 2.0 * curand_uniform(localRandState) - 1.0, 2.0 * curand_uniform(localRandState) - 1.0, 2.0 * curand_uniform(localRandState) - 1.0 ); if (p.LengthSquared() >= 1.0) continue; return p; } }
C++
복사

3.2 부연 설명: Rejection sampling

(−1, −1, −1) ~ (1, 1, 1) 박스에서 랜덤 점을 뽑는다.
단위 구 밖이면 버리고 다시 뽑는다.
분포가 단순하고 구현이 쉬워 학습용으로 적합하다.

4. 감마 보정(Gamma correction)

4.1 문제

난반사에서 감쇠(0.5^n) + 멀티샘플 평균이 들어가면 출력이 “생각보다 훨씬 어두운” 느낌이 날 수 있다.
또한 모니터 출력은 선형이 아니라 감마 특성이 있어, 선형 공간에서 계산한 값을 그대로 출력하면 명도 재현이 맞지 않는다.

4.2 해결: gamma=2.0 보정(sqrt)

가장 단순한 형태로 sqrt()를 적용한다.
col[0] = sqrt(col[0]); col[1] = sqrt(col[1]); col[2] = sqrt(col[2]);
C++
복사
보정 전
보정 후
설명
0.25
0.5
어두운 영역이 밝아짐
0.5
0.707
중간 톤 보정
1.0
1.0
밝은 영역 변화 없음

5. GPU 스택 크기 설정(안정성)

가상 함수(Hit) 호출 + 루프 내 지역 변수 사용 등으로 기본 스택이 부족한 환경이 있을 수 있어 제한을 늘려 안정적으로 동작하도록 했다.
checkCudaErrors(cudaDeviceSetLimit(cudaLimitStackSize, 8192)); // 8KB
C++
복사
재귀를 루프로 바꿔도, vtable 호출/지역 변수/컴파일 옵션에 따라 스택 사용량은 커질 수 있다.
문제가 생기면 이 설정이 빠른 해결책이 된다.

렌더링 결과

해상도: 1440 × 720
샘플 수: 30 samples/pixel
최대 반사 깊이: 50
구체 재질: 회색빛 무광택(matte)
배경: 흰색 → 하늘색 그라디언트
출력 파일: output.ppm