Shadows
그림자가 있으면 물체가 표면 위에 떠 있는지, 접촉하고 있는지 등 공간적인 정보를 알 수 있다.
이를 통해 물체의 방향이나 위치를 쉽게 인지할 수 있기 때문에, 특히 게임과 같은 상호작용 환경에서 유용하다.
그림자는 빛의 방향과 강도에 따라서 영향받는다.
그림자를 생성하는 물체. 즉, 빛을 가리는 물체를 creator라고 부르며
그림자가 드리워지는 표면을 receiver라고 부른다.
물론 물체가 자체적으로 그림자를 생성하고, 동시에 자신의 표면에 그림자를 받을 수도 있다.
그림자는 빛의 유형(점광원 vs 면광원)에 따라 그림자의 경계와 형태가 달라진다.
- 점광원 (Point source):
- 빛이 하나의 점에서 나온다.
- 그림자는 경계가 뚜렷한 엄브라(umbra)(완전 그림자)만 생성된다.
- 면광원 (Area source):
- 빛이 면적을 가지며 발산된다.
- 그림자는 두 가지 영역으로 나뉜다
- 엄브라(umbra): 빛이 완전히 차단된 영역.
- 페넘브라(penumbra): 빛이 부분적으로만 차단되어 그림자가 점진적으로 밝아지는 영역.
위에서 봤던 Point Light와 Area Light를 통해 그림자의 경계를 뚜렷하게 표현할 것인지, 부드럽게 표현할 것인지 선택할 수 있다.

- 하드 그림자 (Hard Shadows):
- 점광원(Point Light Source)에 의해 생성.
- 그림자의 경계가 뚜렷하고 선명하게 나타남.
- 소프트 그림자 (Soft Shadows):
- 면광원(Area Light) 또는 유사한 방식에서 생성.
- 그림자 경계가 부드럽고 점진적으로 흐려짐
그림자 맵핑 (Shadow Mapping)
현재 가장 널리 사용되고 있는 기술 중 하나이다.
빛의 시점에서 장면을 렌더링하여 "깊이 정보"를 기반으로 그림자를 계산한다.
이 과정에서 하얀색은 멀리 있는 객체를, 검은색은 가까운 객체를 나타낸다.
그림자 맵을 구축하는 과정은 다음과 같다.
빛의 위치에 카메라 배치
- 점광원(Point Light)의 위치를 기준으로 가상의 카메라를 설정.
- 여기서 카메라는 빛이 비추는 방향을 바라보는 역할.
- 이때 빛이 보는 장면을 렌더링하게 된다.
장면 렌더링 및 깊이 계산
- 빛의 방향으로 광선을 쏘아 객체와 충돌하는 가장 가까운 깊이(depth)를 계산
- 이 깊이 정보는 각 픽셀의 거리값(빛과 가장 가까운 물체까지의 거리)이다.
- 계산된 깊이 값은 깊이 버퍼(Depth Buffer)에 저장.
- 빛의 시점에서 본 객체들의 가장 가까운 표면 깊이를 기록한다.
텍스처에 저장
- 깊이 버퍼에 저장된 값을 그림자 맵(Shadow Map)이라는 텍스처로 저장한다.
- 이 텍스처는 나중에 그림자 여부를 판단할 때 사용된다.
이후 카메라의 시점에서 장면을 렌더링할 때, 각 픽셀에 대해 검사한다.
- 빛의 방향에서 객체까지의 깊이 값을 그림자 맵에서 가져오고
- 카메라의 시점에서 계산된 깊이 값과 비교한다.
- 일치하면: 빛이 닿고 있는 영역 (빛이 닿은 픽셀)
- 더 크면: 그림자 영역 (빛이 가려진 픽셀)
요약하자면
- 빛의 시점에서 깊이 버퍼를 생성 (그림자 맵 생성)
- 카메라의 시점에서 렌더링하면서 그림자 맵과 비교
- 빛이 닿는 부분과 그림자 부분을 계산하여 최종 화면에 반영
이렇게 점광원의 위치에서 장면이 렌더링되고, 각 그림자 광선이 객체와의 충돌 지점을 계산한다.
하지만 그림자 맵을 사용한 경우, 앨리어싱 문제로 인해 그림자가 부자연스럽다는 단점이 있다.
반면, 레이 트레이싱 기반으로 정확한 하드 그림자를 계산한 경우, 그림자가 깔끔하고 정확하게 나타난다.
Percentage Closer Filtering - PCF
위에서 말했던 앨리어싱 문제를 완화하고, 그림자 경계를 부드럽게 표현하기 위해 PCF 기법이 생겼다.
PCF는 다음과 같은 방식으로 이루어진다.
- 단일 샘플링 대신, 그림자 맵의 주변 여러 좌표를 조회.
- 각 조회 값이 그림자 내부인지 외부인지 확인.
- 그림자 내부에 있는 샘플의 비율을 계산하여 조명 강도를 조정.
이렇게 PCF를 사용하면 하드 그림자에 비해 부드러운 그림자가 생성된다.
기존의 Hard Shadows는 단일 조회로 그림자를 생성하며, 경계가 뚜렷하고 거친 앨리어싱 문제가 있었다.
반면, PCF 쉐도우는 각 프래그먼트당 여러 샘플을 조회(예: 16회 조회)하여 부드러운 경계를 생성한다.
(PCF의 다중 샘플링으로 그림자 경계를 부드럽게 처리. 품질은 향상되지만 성능 부담이 있긴 함)
위에서 말했던 다중 샘플링은 특정 지점(P)의 그림자 여부를 판단하기 위한 것으로
다음과 같은 과정을 갖는다.
- 빛의 위치(PL)에서 지점(P)까지 그림자 광선을 추적한다.
- 그림자 맵에서 지점(P) 주변의 여러 샘플 값을 가져온다.
- 각 샘플 값이 실제로 그림자 안에 있는지(즉, 빛의 경로가 차단되었는지)를 확인한다.
- 그림자 안에 포함된 광선의 비율을 계산하여 그 지점의 밝기를 결정한다.
이런 과정을 통해서 그림자 안에 포함된 광선이 많을수록, 해당 지점은 더 어두워지게 된다.
이 방법은 단순히 그림자를 "있다/없다"로 판정하는 것이 아니라, 그림자의 부드러운 전환을 가능하게 한다.
아래 이미지는 Area Light에 의해 생성된 그림자다.
면광원(Area Light)은 점광원(Point Light)과 달리 면적을 가지므로 빛이 여러 방향에서 나온다.
이런 이유 때문에 광선의 일부가 차단되면서 생기는 Soft Shadows가 생긴다.
Area Light는 여러 광원에서 지점(P)까지 광선을 추적하고, 광선이 장애물에 의해 차단되는지를 계산한다.
이 방법 역시 차단된 광선의 비율을 바탕으로 해당 지점의 밝기를 결정한다.
광선이 전부 차단되면 해당 지점은 완전한 그림자(엄브라, Umbra)로 판단하고,
일부 광선만 차단되면 부분 그림자(페넘브라, Penumbra)가 형성되어 점진적으로 밝아진다.
그림자 볼륨 (Shadow Volumes)
그림자 볼륨은 빛에 의해 생성된 3D 그림자 공간을 정의하는 기법이다.
특히 복잡한 다각형 구조의 정확한 그림자 계산에 적합하다.
우선, 각 폴리곤(다각형)의 가장자리에서 광선을 연장하고, 투영 쿼드(Quad)를 생성하여 그림자 볼륨을 만든다.
여기에서 만든 그림자 볼륨 내부의 모든 공간은 그림자 영역으로 간주한다.
그림자 볼륨을 렌더링하기 위해서 지점이 그림자에 속하는지를 판단해야 한다.
이를 위해 해당 지점과 카메라 사이의 다각형 개수를 계산한다.
이 때, 프론트 페이싱은 카메라를 향하고 있는 면
백 페이싱은 카메라 반대 방향을 향하고 있는 면을 의미한다.
특정 지점에서 백 페이싱보다 프론트 페이싱 다각형을 더 많이 통과한 경우, 그 지점은 그림자 내부에 있다고 판단한다.
각 기법들의 차이점
- PCF는 다중 샘플링을 통해 그림자의 부드러운 전환을 계산하고, 각 샘플에서 빛의 차단 여부를 기반으로 밝기를 결정
- 면광원 그림자는 광선의 일부만 차단되면서 부드러운 전환 영역(페넘브라)을 생성
- 그림자 볼륨은 빛의 방향에 따라 다각형의 가장자리를 연장하여 3D 공간에서 그림자 영역을 정의하는 기법
Stencil Buffer
스텐실 버퍼는 화면상의 픽셀에 대해 추가적인 정보(종종 8비트)를 저장하는 버퍼이다.
각 픽셀에서 덧셈, 뺄셈 등의 연산을 통해 값을 변경하며, 이 값을 기반으로 이후의 렌더링 단계를 제어할 수 있다.
스텐실 버퍼에 그림자 볼륨 정보를 기록해서 특정 픽셀을 마스킹(그리기 제한)하는데,
그림자 볼륨의 정보는 내부와 외부를 판별하는 데 사용되고
그림자 내부 영역은 스텐실 값의 증가 및 감소를 통해 계산된다.
스텐실 버퍼를 활용한 그림자 볼륨 렌더링 프로세스
- 장면을 주변광(ambient lighting)만으로 렌더링한다.
- 이 단계에서는 Z-버퍼 업데이트와 색상 버퍼 쓰기를 비활성화하고, 스텐실 버퍼만 수정한다.
- 프론트 페이싱 다각형(그림자 볼륨의 앞면)을 스텐실 버퍼에 렌더링한다.
- 프론트 페이싱 다각형을 통과할 때마다 스텐실 count 값을 증가시킨다.
- 백 페이싱 다각형(그림자 볼륨의 뒷면)을 스텐실 버퍼에 렌더링한다.
- 백 페이싱 다각형을 통과할 때마다 스텐실 count 값을 감소시킨다.
- 스텐실 버퍼의 값이 0인 영역에 대해 확산광(diffuse)과 반사광(specular)을 렌더링한다.
- 이 값이 0이 아닌 경우, 해당 픽셀은 그림자 내부에 있다고 간주된다.
볼륨 병합 (Merging Volumes)
빛을 향해 있는 두 다각형이 같은 엣지를 공유할 경우, 불필요한 계산이 발생한다.
이를 해결하기 위해서 두 다각형이 공유하는 엣지에서 생성되는 두 개의 쿼드(Quad)는 서로 상쇄되도록 설정한다.
위 그림을 보면 내부 엣지(Interior Edge)가 두 개의 쿼드를 생성하지만, 이들은 서로 취소되어 실제 그림자 볼륨에는 영향을 미치지 않고있다.
윤곽선 엣지 (Silhouette Edges)
빛의 시점에서 볼 때, 내부 엣지(Interior Edge)는 그림자 볼륨 생성에 기여하지 않는다.
그래서 빛의 방향에서 보이는 물체의 윤곽선 엣지(Silhouette Edges)만 찾아서 그림자 볼륨을 생성한다.
(내부 엣지를 제거함으로써 많은 불필요한 다각형을 제거하여 성능이 향상됨.)
Ambient Occlusion (AO)
Ambient Occlusion은 물체의 표면에서 주변광(환경광)의 차단 정도를 계산하여 음영을 생성하는 기술이다.
환경광이 차단될수록 어두워지고, 개방된 공간일수록 밝아진다.
위에서 배웠던 shadow를 통해 주변 환경의 그림자를 통한 음영을 계산할 수 있다.
(물체가 만나거나 가까워지는 부분에 어두운 음영을 추가함)
주요 아이디어는 특정 지점(P)을 기준으로, 주변 반구(Hemisphere)의 차단된 비율(occluded fraction)을 계산하는 것이다.
AO 값은 차단된 광선의 비율에 따라 환경광의 세기를 줄이는 방식으로 적용된다.
- Op: 특정 지점에서의 AO 값.
- Vd(ω): 방향 ω에 대한 가시성 함수(차단 여부: 1은 차단됨, 0은 차단되지 않음).
- N⋅ω: 법선 벡터와 광선 방향의 내적.
- Ω: 반구 영역.
실제 계산은 복잡하므로, 적은 샘플 수를 사용하여 근사치를 계산한다. (오른쪽)
Screen-Space Ambient Occlusion (SSAO)
SSAO는 장면의 깊이 버퍼를 사용하여 AO를 빠르게 계산할 수 있다.
이를 통해 실시간 렌더링을 원활하게 할 수 있다.
SSAO를 구현하기 위해서 우선 각 픽셀의 깊이 정보를 저장할 수 있는 깊이 버퍼를 생성해야 한다.
다음으로, 각 픽셀 에서 깊이 버퍼를 기반으로 주변 반구에서 광선의 차단 정도를 추적한다.
(일부 샘플만 사용하여 효율적으로 계산함)
계산된 AO 맵(occlusion map)을 블러 처리하여 노이즈를 줄이고,
마지막으로 AO 값을 사용해 픽셀을 셰이딩한다.
여기에서 AO 값이 높을수록 환경광을 어둡게 조정한다.