Company
교육 철학

피사계 심도(Depth of Field) 또는 디포커스 블러(Defocus Blur)

개요

이전 챕터(Chapter 10)까지의 카메라는 핀홀 카메라(pinhole camera) 모델이라, 장면의 어떤 거리든 동일하게 선명하게 보였다.
하지만 실제 카메라는 렌즈와 조리개가 있기 때문에, 특정 거리(초점 거리, focus distance) 에 있는 물체만 선명하고 그 앞/뒤는 흐려진다. 이 흐림을 피사계 심도(Depth of Field) 또는 디포커스 블러(Defocus Blur) 라고 한다.
Chapter 11에서는 렌즈를 물리적으로 정확히 시뮬레이션하기보다는, 레이트레이서에서 널리 쓰는 단순화 모델을 사용한다.
카메라의 원점을 “점”이 아니라 원반(disk) 위의 임의의 점으로 만든다.
모든 레이는 초점 평면(focus plane) 위의 같은 목표점을 향하도록 만든다.
이렇게 하면 초점 평면에 있는 물체는 여러 샘플이 같은 점으로 모이므로 선명하게 나오고, 초점 평면 밖의 물체는 샘플마다 약간씩 다른 위치를 보게 되어 평균되면서 흐려진다.

용어 정리 (사진 용어와 1:1 대응)

aperture(조리개): 렌즈 구멍 크기. 클수록 흐림이 강함
lens radius: aperture / 2
focusDist(초점 거리): 카메라에서 초점 평면까지의 거리
bokeh(보케): 초점 밖 하이라이트가 원형으로 번지는 느낌 (circle of confusion)

핵심 아이디어: “렌즈에서 랜덤 샘플링”

핀홀 모델은 모든 레이가 origin 한 점에서 출발한다.
반면 DOF 모델은 레이가 렌즈 원반 위의 서로 다른 점에서 출발한다.
flowchart LR
	L["렌즈 원반 위 랜덤 점 (origin + offset)"] -->|"같은 픽셀 목표"| F["초점 평면의 목표점"]
	L -->|"다른 출발점"| S["센서/이미지 평면에서 평균"]
Mermaid
복사
초점 평면 위의 점은 모든 샘플이 같은 곳을 “정확히” 향하므로 선명
초점 평면 밖의 점은 샘플마다 교차 위치가 달라 평균되며 흐림

RandomInUnitDisk: 렌즈 위의 랜덤 점

렌즈 원반은 2D(카메라 로컬 u-v 평면)에서 샘플링하면 된다.
inline vec3 random_in_unit_disk(rng& r) { while (true) { auto p = vec3( r.uniform(-1.0, 1.0), r.uniform(-1.0, 1.0), 0.0 ); if (p.length_squared() >= 1.0) continue; return p; } }
C++
복사
CUDA에서는 curand_uniform()을 쓰면 되고, 로직 자체는 동일하다.

초점 평면 배치: 뷰포트를 focusDist로 스케일

Chapter 10에서는 뷰포트가 카메라에서 “거리 1”에 있다고 보면 된다.
DOF에서는 초점 평면을 focusDist에 두고 싶으니, 뷰포트의 크기와 위치를 focusDist만큼 스케일한다.
Chapter 10과 동일하게 vfov로 halfHeight를 만든 다음,
halfHeight = h=tan(θ/2)h = \tan(\theta/2)
halfWidth = aspect · hh
여기서 뷰포트가 초점 평면에 놓이도록 focusDist를 곱한다.
viewportHalfHeight = focusDist · halfHeight
viewportHalfWidth = focusDist · halfWidth
horizontal=2(focusDisthalfWidth)u\mathbf{horizontal} = 2 \cdot (\mathrm{focusDist}\cdot\mathrm{halfWidth}) \cdot \mathbf{u}
vertical=2(focusDisthalfHeight)v\mathbf{vertical} = 2 \cdot (\mathrm{focusDist}\cdot\mathrm{halfHeight}) \cdot \mathbf{v}
lowerLeft=origin(focusDisthalfWidth)u(focusDisthalfHeight)vfocusDistw\mathbf{lowerLeft} = \mathbf{origin} - (\mathrm{focusDist}\cdot\mathrm{halfWidth})\,\mathbf{u} - (\mathrm{focusDist}\cdot\mathrm{halfHeight})\,\mathbf{v} - \mathrm{focusDist}\,\mathbf{w}
이렇게 해두면 “초점 평면 위에서의 픽셀 위치”가 기준점이 된다.

GetRay: 렌즈 오프셋(offset) 적용

렌즈 샘플링은 카메라 로컬 좌표계에서 오프셋을 만들면 된다.
rd는 렌즈 원반 샘플
offset = u * rd.x + v * rd.y
그 다음 레이는 다음처럼 만든다.
ray get_ray(double s, double t, rng& r) const { vec3 rd = lensRadius * random_in_unit_disk(r); vec3 offset = u * rd.x() + v * rd.y(); point3 rayOrigin = origin + offset; point3 target = lowerLeftCorner + s*horizontal + t*vertical; return ray(rayOrigin, target - rayOrigin); }
C++
복사
같은 (s, t) 픽셀이어도 매번 rayOrigin이 달라지므로,
초점 평면 밖의 물체는 서로 다른 광선을 통해 샘플링되고 평균되면서 흐려진다.

파라미터가 결과에 미치는 영향

파라미터
작은 값
큰 값
aperture
핀홀에 가까움. 대부분 선명
강한 보케. 초점 밖이 많이 흐림
focusDist
가까운 물체에 초점
먼 물체에 초점
samples per pixel
흐림이 노이즈처럼 거칠게 보일 수 있음
보케가 부드럽고 자연스럽게 평균됨
현재 설정의 aperture = 2.0은 꽤 큰 값이라 DOF가 눈에 띄게 나온다.

변경 파일 요약

Camera.h
random_in_unit_disk (또는 RandomInUnitDisk) 추가
생성자에 aperture, focusDist 파라미터 추가
get_ray (또는 GetRay)에 RNG 상태 전달
뷰포트를 focusDist로 스케일해서 초점 평면 배치
렌더 커널에서 GetRay(u, v, &localRandState) 호출
CreateWorld에서 카메라 세팅: lookfrom/lookat, aperture, focusDist

장면 설정 (현재 값)

lookfrom = (3, 3, 2)
lookat = (0, 0, -1)
vfov = 20°
aperture = 2.0
focusDist ≈ |lookfrom - lookat| (장면 중심 근처에 초점)