개요
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




