서비스
교육 철학

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
복사
1.1.1 왜 p + n + random 형태인가?
레이는 보통 Ray(origin, direction) 형태다. 난반사에서 새 레이는 hit 지점에서 다시 출발해야 하므로 origin = p로 둔다.
•
target = p + n + r (여기서 r = random_in_unit_sphere())
•
direction = target - p = n + r
즉 p는 “방향”을 만들기 위한 기준점이고, 실제로 방향을 결정하는 핵심은 n + r이다.
1.1.2 왜 반구(hemisphere) 쪽으로 나가게 되는가?
r만 쓰면 모든 방향(구 전체)으로 균일하게 뽑히는 반면, n을 더하면 분포의 중심이 법선 방향으로 이동한다.
그 결과 n + r는 통계적으로 법선과 같은 쪽(표면 바깥쪽)으로 더 자주 향한다.
(개념) - r: 원점 주변(구 전체) 랜덤 - n + r: 중심이 n으로 이동 → 법선 쪽 반구로 “치우친” 분포
Plain Text
복사
1.1.2.1 그림으로 보는 직관
(1) hit 지점 p와 법선 n ↑ n | | 입사 레이 → ● p ↗ ↑ ↖ (새 레이가 여러 방향으로 퍼짐) (2) r = random_in_unit_sphere() 는 '원점 주변' 랜덤 벡터 unit sphere (반지름 1) _______ / \ | • r | \_________/ (3) n을 더하면 분포 중심이 n 쪽으로 이동 r 만 쓰면: n + r 를 쓰면: (구 전체) (법선 방향으로 치우침) ↖ ↑ ↗ ↖ ↑ ↗ \|/ \ | / ← ---•--- → ← ---●--- → /|\ / | \ ↙ ↓ ↘ ↙ ↓ ↘ (아래쪽도 많이 나옴) (위쪽(법선 쪽)으로 더 자주 나옴)
Plain Text
복사
1.1.3 더 정확한 Lambertian(코사인 가중치)과의 차이
이 방식(n + random_in_unit_sphere)은 구현이 단순해서 학습용으로 좋지만, 물리 기반으로 더 “정확한” Lambertian은 법선과 이루는 각도 θ에 대해 cos(θ)로 가중된 분포(cosine-weighted hemisphere)를 따른다.
•
여기서는 간단히 “법선 주변으로 랜덤하게 튄다”를 흉내 내는 버전
•
더 정확한 샘플링은 별도의 샘플링 기법(예: cosine-weighted hemisphere sampling)을 사용한다.
1.1.4 실전 팁: 거의 0 방향 벡터 방지
드물게 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