티스토리 뷰
컴퓨터로 원근감을 표현하는 방법 (feat. Depth Buffer)
시뮬레이션 프로그래머 2025. 3. 5. 00:05
원근감이란 멀고 가까운 거리에 대한 느낌. 미술에서는 색채·명암·선 등을 이용하여 나타냄.
Depth Buffer란?
3D 장면을 2D 화면(모니터)에 객체를 그릴 때(Rasterization) 카메라로부터 멀리 떨어진 객체를 먼저 그리고 가까운 객체를 다음에 그리도록 순서를 조절해야 원근감이 올바르게 표현된다.
하지만 같은 거리에 있는 두 객체가 서로 겹쳐 있다면, 물체 단위로 그리는 순서를 조절하는 것으로는 원근감을 제대로 표현할 수 없다.
⇒ 이 문제의 근본적인 해결 방법은 각 객체의 단위가 아닌 객체를 구성하는 삼각형의 픽셀 단위로 깊이를 비교하고 가까운 곳에 있는 픽셀만 그리는 것이다. 이 과정을 Depth Testing이라고 한다.
Depth Testing은 각 픽셀의 깊이 값을 비교하여 더 가까운 픽셀만 화면에 렌더링 하는 기법이다.
이 과정에서 Depth Buffer(Z-buffer)를 사용하여 각 픽셀의 Z값을 저장하고 갱신하면서, 원근감을 정확하게 표현한다.
하지만 깊이 값을 저장하는 Depth Buffer는 정밀도가 제한적이기 때문에 Z-fighting(깊이 충돌 현상)이 발생할 수 있다.
정리하면, 3D 장면을 모니터에 표현할 때, 원근감을 표시하기 위해 Depth Buffer를 이용해 Depth Tesitng을 진행한다.
하지만, 같은 깊이(Z값)를 가지는 두 개의 삼각형이 픽셀 단위에서 깊이 비교 연산을 수행할 때, 부동소수점 정밀도 한계로 인해 Z-fighintg(깜빡이거나 어긋나는 현상) 문제가 발생한다.
모니터에 2D 와 3D를 표현할 때(투영) 차이점
그래픽을 화면에 출력할 때, 직교 투영(Orthographic Projection)과 원근 투영(Perspective Projection) 방식이 있다.
이 두 방식은 깊이(Depth) 값의 변화 방식과 Z-fighting 발생 가능성에서 큰 차이를 보인다.
Orthographic Projection (직교 투영)
2차원 그래픽을 모니터에 투영시킬 때는 깊이 값이 선형적으로 변한다.
예를 들어, 모나리자 그림을 모니터로 볼 때 멀리서 보든 가까이서 보든 그림의 형상은 같은 비율을 유지한다.
즉, 모니터는 2차원 그래픽을 표현할 때 깊이 값을 조정할 필요 없이 단순한 색상 정보만 렌더링 하면 된다.
결론적으로, 2D 그래픽에서는 원근법이 적용되지 않으므로 깊이 값(Z)이 선형적으로 유지되므로, 깊이 버퍼를 균등하게 사용할 수 있으며, Z-fighting 문제도 거의 발생하지 않는다.
Perspective Projection (원근 투영)
3차원 그래픽을 모니터에 투영시킬 때는 원근감을 표현하기 위해 깊이 값이 비선형적으로 변한다.
예를 들어, 배틀그라운드 게임을 할 때 내 시점$(z_{eye})$과 가까운 적은 크게 보이고, 멀리 있는 적은 작게 보여야 한다.
즉, 3D 그래픽에서는 원근법이 적용되므로 깊이 값(Z)이 픽셀마다 다르다.
따라서 Z-buffer(깊이 버퍼)에 저장되는 값이 비선형적으로 변한다.
예를 들어, 배틀그라운드에서 내 시점$(z_{eye})$으로부터 적이 멀어질수록 정밀도가 급격히 낮아진다.
(거리가 멀어질수록 정밀도가 낮아지는 이유는 아래 원근투영 하는 과정에서 설명한다)
이로 인해 깊이 값이 제대로 구별되지 않아 다른 값을 같은 값으로 인식하는 문제가 발생하고, 이는 Z-fighting을 일으킨다.
잘 와닿지 않을텐데 Z-fighting이 일어나는 상황을 상상하기 위해 아래 사진을 보자.
주황옷과 흰 옷을 입은 캐릭터가 겹쳐있다.
이때 만약 부동소수점 정밀도로 인해 깊이 값 연산 중에 오차가 발생하여 Z-fighting이 발생한다면 흰옷 캐릭터와 주황옷 캐릭터가 서로 자기가 모니터에 보이기 위해 깜박일 것이다.
하지만, 현재는 주황옷이 깊이 값 우선순위가 높기 때문에 주황옷이 더 앞에 보이는 것이다.
그리고 주황옷 캐릭터가 하나의 객체로 인식되는게 아니라, 오른쪽 사진처럼 각각의 픽셀마다 1x1 형태로 매 프레임마다 깊이 값(Depth Buffer)이 저장되고 해당 픽셀이 표현되는 것이다.
Depth Buffer 변환 과정
3D 그래픽스 파이프라인 과정
- 로컬 좌표 (Local Space / Object Space): 객체의 원래 좌표
- 월드 좌표 (World Space): 장면(Scene) 내에서 객체의 위치를 나타내는 좌표
- 뷰 좌표 (View Space): 카메라(시점) 기준으로 변환된 좌표
- 클립 좌표 (Clip Space): 원근 투영 변환이 적용됨
- NDC (Normalized Device Coordinates) → $z_{ndc}$인 깊이 값이 [-1, 1] 범위로 정규화
- 뷰포트 좌표 (Viewport Space) → OpenGL에서는 glDepthRange(n_range, f_range)함수로$z_{window}$인 깊이 값을 [0, 1] 범위로 정규화
마지막 뷰포트 좌표로 변환 과정에서, 깊이 값을 [0, 1]로 정규화하고 Depth Buffer에 저장한다.
$z_{ndc}$ 변환 수식
Perspective Projection에서는 멀리 있는 객체가 작아 보이도록 하기 위해 깊이 값을 $\frac{1}{z_{eye}}$ 형태로 변환된다.
$
z_{ndc} = \frac{f + n}{f -n} + \frac{2fn}{z_{eye}(f-n)}
$
$z_{ndc} ∝ \frac{1}{z_{eye}}$
⇒ 즉, 원근 투영 변환과정에서 Depth( $z_{ndc}$)가 원거리에서 급격히 작아진다.
원근 투영 변환 특징
- Depth Buffer 정밀도가 Near Plane에 집중되고, Far Plane에서는 정밀도가 거의 없음
- Near Plane에 가까울수록 정밀도가 높음
- Far Plane에 가까울수록 정밀도가 낮음 → 원거리 Z-fighting 발생
- $\frac{Far}{Near}$ 비율이 클수록 비선형성이 심해짐
$z_{window}$ 변환 수식
NDC는 -1에서 1 사이의 값을 가지지만, 최종 깊이 값(Depth Buffer)은 0에서 1 사이의 값을 가진다.
이를 변환하는 OpenGL 함수: glDepthRange(n_range, f_range)
깊이 값이 0에서 1사이의 값을 가진다면, n_rage = 0
, f_range = 1
$
z_{window} = \frac{1}{2}(\frac{f+n}{f-n} + \frac{2fn}{z_{eye}(f-n)}) + \frac{1}{2}
$
$z_{window} ∝ \frac{1}{z_{eye}}$
OpenGL Depth Buffer Format
16-bit Fixed Point (고정소수점)
정밀도가 낮고 깊이 충돌(Z-fighting)이 자주 발생한다.
24-bit Fixed Point (고정소수점, 대부분 사용됨)
대부분의 그래픽 카드에서 지원하는 표준 깊이 버퍼
깊이 값이 균등하게 분포되어 있지만, 원거리에서 정밀도가 떨어지는 문제 발생
32-bit Floating Point (부동소수점)
정밀도가 비선형적으로 분포된다.
0 근처에서 값이 촘촘하고, 멀리 갈수록 값이 드문드문 배치된다.
0 근처의 값을 이용한다면, 원거리에서 더 많은 정밀도를 제공할 수 있다.
Depth Buffer Precision Problem Solutions
1. Adjust near/far plane
2. Remove Distant Objects
3. Complementary Depth Buffering
4. Logarithmic Depth
5. Buffer Multi Frustum Rendering
Adjust near/far plane
Set Near Plane further away
Basic Idea: Near Palne을 가능 한 멀리 설정해서, Depth Buffer Precision을 높이자.
예시)
$Near = 0.1m, Far = 1,000m$ → $Far/Near = 10,000$
$Near = 10m, Far = 1,000m$ → $Far/Near = 100$ (더 좋은 정밀도)
Set Near Plane further away 문제점
Near Plane을 너무 멀리 설정하면, 가까운 객체가 잘리거나 내부를 볼 수 있는 Clipping Artifacts
가 발생함.
Set Near Plane further away 문제점 해결 방법
1. Near Plane에 가까워지면 객체를 점진적으로 투명하게 만들기 - Fade Out
2. Blending을 사용하여 부드럽게 사라지도록 처리 - Fade Out
Set Far Plane Close
Basic Idea: $\frac{Far}{Near}$ 에서 Far를 줄여, 비율이 낮아지고 Depth Buffer Precision을 개선하자.
하지만, Far Plane을 당기는 것보다, Near Plane을 당기는 것이 효율적이다.
예시)
$Near = 1m, Far = 1,000m$ → $Far/Near = 1,000$
$Near = 2m, Far = 1,000m$ → $Far/Near = 500$ (큰 차이)
$Near = 1m, Far = 500m$ → $Far/Near = 500$ (차이 적음)
Set Far Plane Close 문제점
Far Plane을 줄이면 먼 곳의 객체가 갑자기 사라질 수 있음
Set Far Plane Close 문제점 해결 방법
먼 거리 객체를 Blending 또는 Fog(안개 효과)를 사용해서 부드럽게 사라지게 처리 - (Fade Out)
Near/Far Plane을 조정을 통한 성능 개선 (Frustum Culling, GPU Z-cull)
1. Near/Far Palne을 가깝게 설정하면 더 많은 객체가 Frustum Culling
으로 제거된다.
2. GPU Z-cull 최적화 효과도 적용된다.
2.1 GPU는 Tile 단위로 Z 값을 비교해 렌더링 할 필요 없는 픽셀을 미리 제거한다.
2.2 Near/Far 비율이 작을수록 타일의 깊이 정밀도가 향상되어 더 많은 픽셀이 제거된다. → 성능 최적화
Remove Distant Objects
Basic Idea: 너무 먼 객체는 렌더링 할 필요가 없다. → 제거해서 성능 향상
특히 텍스트나 UI 요소(빌보드, 표지판 등)는 멀리 있어도 크기가 줄어들지 않기 때문에 많으면 화면이 복잡해진다.
따라서 카메라시점과 멀어지는 객체를 점진적으로 제거한다. (Fade Out)
LOD(Level of Detail)와 유사한 기법
Complementary Depth Buffering
Basic Idea: Depth 값을 반대로 저장하여, 멀리 있는 객체의 Depth Buffer Precision을 높이자.
Unity의 Reverse Z기법과 유사한 개념
일반적인 Depth Buffer 기본 설정
Depth Buffer 초기화 값(Clear Depth): 1
Depth 비교 함수(Depth Comparison Function): "LESS" → 작은 값일수록 더 가까움
Near/Far Plane 관계: Near < Far
Complementary Depth Buffering을 적용
Depth Buffer 초기화 값(Clear Depth): 0
Depth 비교 함수(Depth Comparison Function): "GREATER" → 큰 값일수록 더 가까움
Near/Far Plane 관계: Near > Far
⇒ Depth 저장 방식을 반대로 뒤집는 방식
Complementary Depth Buffering을 적용 시, Depth Precision이 개선되는 이유
부동 소수점(Floating Point) 특징을 활용한다.
부동 소수점 수는 0
에 가까울수록 값이 더 촘촘하게 분포된다.
⇒ 즉, 깊이 값을 반대로 Far Plane 쪽을 0으로 저장하면, 가장 정밀한 영역이 Far Plane 쪽에 배치된다.
Fixed Point (24-bit 정수형) 깊이 버퍼에서도 효과가 있지만,
Floating Point (32-bit 부동소수점) 깊이 버퍼에서 원거리 정밀도가 더 극적으로 향상된다.
⇒ Z-fighting 문제가 심각한 경우 32-bit Floating Point와 함께 사용하면 효과가 극대화된다.
Logarithmic Depth Buffer
Basic Idea: Depth Buffer의 Depth 값을 로그 함수로 변환하여 멀리 있는 객체의 Depth Buffer Precision을 높이자.
주의: Log Depth 변환은 반드시 Fragment Shader에서 수행해야 한다.
Logarithmic Depth 수식
$
z_{clip}= \frac{2\ln(Cz_{clip}+1)}{\ln(Cf+1)-1}
$
변수 설명
$C$: 가변 상수 → Near/Far 거리 정밀도를 조절하는 역할
$C$ 값을 작게 하는 경우: 원거리 정밀도 증가, 가까운 곳 정밀도 감소
$C$ 값을 크게 하는 경우: 가까운 곳 정밀도 증가, 원거리 정밀도 감소
$f$: Far Plane
$z_{clip}$: Clip Space 깊이 값, 투영 변환 이후 Perspective Divide(원근 분할) 직전에 존재하는 좌표 공간
수식 의미
원래 깊이 값($z_{clip}$)을 로그 함수로 변환하여 저장한다.
가까운 곳에서는 정밀도를 약간 손해 보지만, 원거리 정밀도를 극적으로 증가시킨다.
$C$ 값을 조정하면 가까운 곳과 먼 곳의 정밀도 균형을 맞출 수 있다.
Logarithmic Depth Buffer 장점
1. Z-Fighting 문제 해결 → 원거리에서 Depth Buffer Precision이 증가하여 지터링을 줄인다.
2. 기존의 Depth Buffer보다 원거리 정밀도가 훨씬 우수 → 특히 Virtual Globe 같은 초대형 장면(Scene)에서 필수
3. OpenGL의 Standard Depth Buffer를 그대로 활용 가능 → Depth Buffer 자체를 수정할 필요 없이 Shader에서 변환만 적용하면 된다.
- Vertex Shader에서 적용 시, 아티팩트가 발생할 수 있다.
- 방법: $z_{clip}$ 값을 Log 변환 후, $w_{clip}$을 곱해 원근 분할을 보정
- 문제점: $w_{clip}$을 사용한 선형 보간(Interpolation) 과정에서 아티팩트가 발생할 수 있음
- $z_{ndc} = z_{clip} / w_{clip}$ 과 같이 정규화 하면 선형적이다. 하지만,
- $z_{ndc}= \log(z_{clip})$인 경우 비선형적으로 보간되기 때문.
- Rendering Pipeline 참고
void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_Position.z = (2.0 * log(C * gl_Position.z + 1.0)) / (log(C * far + 1.0) - 1.0); }
- Fragment Shader에서 적용(더 나은 방식)
- 방법: Depth 값을 보간한 후, 최종적으로 Fragment Shader에서 Log 변환을 적용
- 장점: Vertex Shader에서의 보간 문제 해결 가능
void main() { float logDepth = (2.0 * log(C * gl_FragCoord.z + 1.0)) / (log(C * far + 1.0) - 1.0); gl_FragDepth = logDepth; }
Logarithmic Depth Buffer 단점
1. 가까운 객체의 정밀도가 떨어진다.
- $C$ 값을 너무 작게 설정하면 가까운 곳에서 깊이 충돌 발생 가능
- 적절한 $C$ 값을 선택하는 것이 중요함
2. Shader에서 추가적인 계산이 필요하다.
- 일반 Depth Buffer와 비교하면 약간의 성능 오버헤드 발생
Multi Frustum Rendering
Basic Idea: Single Frustum을 사용할 경우, Far/Near 비율이 크므로, Multi Frustum을 사용하여, Depth 범위를 나누어서 렌더링하자.
- 여러 개의 Frustum을 계층적으로 나누어 렌더링
- 각 Frustum은 독립적인 Near/Far Plane을 가짐
Multi Frustum Rendering 예시
Globe Rendering: Near/Far Plane이 $1m$ ~ $100,000,000m$인 장면을 렌더링 한다고 가정하자.
- 하나의 Frustum을 사용하면 $\frac{Far}{Near} = \frac{1}{100,000,000}$ 이 되어 Depth Precision 문제 발생
- Multi Frustum을 적용하면?
- 첫 번째 Frustum: $Near = 1m,$ $Far = 1,000m$
- 두 번째 Frustum: $Near = 1,000m,$ $Far = 1,000,000m$
- 세 번째 Frustum: $Near = 1,000,000m,$ $Far = 1,000,000,000m$
- 각 Frustum은 Far/Near 비율이 1,000이므로 Depth Buffer Precision 유지 가능
Multi Frustum Rendering 순서
Back-to-Front: 가장 멀리 있는 Frustum부터 렌더링 한다.
- 가장 먼 Frustum($1,000,000m$ ~ $100,000,000m$)을 먼저 렌더링
- 그다음 가까운 Frustum($1,000m$ ~ $1,000,000m$)을 렌더링
- 가장 가까운 Frustum( $1m$ ~ $1,000m$)을 마지막으로 렌더링
⇒ 가까운 Frustum의 객체가 먼 Frustum의 객체를 overwrite
하며 렌더링 된다.
Multi Frustum Rendering 장점
- Depth Precision 문제 해결 → Z-fighting 방지
- Far, Near Plane 오브젝트 모두 고품질로 렌더링 가능
- 지구, 대규모 맵 등에서 정밀한 렌더링 가능
- 특별한 하드웨어 요구가 없음
Multi Frustum Rendering 단점
- Object와 Frustum간의 매핑을 지속적으로 업데이트 해야 함
- 각 Frustum마다 별도로 렌더링해야 하므로 성능 오버헤드 증가
- Depth값을 기반으로 위치(Position) 정보를 다시 계산하기 어려움 (
Deferred Shading
과 호환성이 떨어짐)
Multi Frustum Rendering 성능 및 최적화 고려사항
Multi Frustum Rendering을 사용할 때, CPU 및 GPU 성능 저하를 피하기 위해 여러 최적화가 필요하다.
1. Frustum Culling Optimization (절두체 컬링 최적화)
문제 상황
- Frustum이 여러 개 존재하면, 각 Frustum마다 모든 객체에 대해 Near/Far Plane 충돌 여부를 확인하는 과정이 많아져 CPU 연산량이 급격히 증가할 수 있음.
- Frustum Culling을 효율적으로 수행하지 않으면, 불필요한 객체에 대해 계속 깊이 테스트가 수행됨 → 성능 낭비
해결책: Hierarchical Frustum Culling (계층적 절두체 컬링)
- 모든 개별 Frustum을 따로 검사하는 것이 아니라, 전체 Frustum을 하나의 대형 Frustum(Frustum Union)으로 묶어서 1차 Culling 수행
- 이후, 각 Frustum별로 더 세부적인 컬링을 수행하여 불필요한 객체를 걸러낸다.
⇒ CPU 연산량이 줄어들고, 불필요한 Frustum 충돌 검사를 방지할 수 있다.
2. In-Frustum Optimization (절두체 내부 최적화)
문제 상황
- 각 Frustum을 렌더링할 때, 모든 객체를 다시 확인해야 한다면 Frustum Culling자체가 비효율적일 수 있음
- 특히, 한 개의 객체가 여러 Frustum에 걸쳐 있으면 여러 번 렌더링될 가능성이 있음
해결책: Frustum별 리스트 관리
- 각 객체를 한 번만 처리하도록 객체 리스트(Linked List) 활용
- 객체가 특정 Frustum에 완전히 포함된 경우, 다음 Frustum에서 다시 검사할 필요가 없으므로 리스트에서 제거
- 프레임이 갱신될 때마다 다시 추가하는 방식으로 관리하면, 렌더링 오버헤드를 줄일 수 있음
3. Minimizing Frustum Count (절두체 수 최소화)
문제 상황
- Frustum 개수가 많으면, 추가적인 연산과 메모리 사용량이 증가하여 성능이 저하될 수 있음
- 특히, Near Plane이 너무 가까운 Frustum은 필요 이상으로 작은 객체까지 포함하게 되어 성능이 비효율적일 수 있음
해결책: Near Plane을 최대한 멀리 설정
- 가까운 Frustum의 Near Plane을 가능한 한 멀리 설정하면, 더 적은 개수의 Frustum으로 동일한 장면을 커버할 수 있음
- 이렇게 하면, 전체적으로 렌더링해야 하는 Frustum 개수를 줄일 수 있어 성능이 향상됨
4. Avoiding Redundant Rendering (객체 중복 렌더링 방지)
문제 상황
- 객체가 두 개 이상의 Frustum에 걸쳐 있으면 중복 렌더링될 수 있음
- 중복 렌더링된 객체는 추가적인 CPU 연산, 버스 트래픽, 정점(Vertex) 처리 비용을 초래함
- 특히 투명 객체(Transparent Objects)는 중복 렌더링 시 시각적 아티팩트가 발생할 수 있음
- 예를 들어, 투명한 객체가 두 개의 Frustum에 걸쳐 렌더링되면, 투명도가 두 배로 적용되어 색상이 더 진해지는 문제 발생
해결책: Overlapping Frustum 설정
- Frustum 간 약간의 오버랩을 추가하여(예: Near Plane = 990m, Far Plane = 1000m), 객체가 두 개 이상의 Frustum에 걸치는 현상을 줄임
- 중복 렌더링을 감지하고, 동일한 객체를 다시 렌더링하지 않도록 관리
- Frustum 내부에서 Z 정렬(Front-to-Back)을 수행하여, GPU의 Early-Z 최적화를 활성화
5. Optimizing Early-Z (Early-Z 최적화 활용)
문제 상황
- 일반적으로 Z-Buffer 최적화를 활용하기 위해, 객체를
Near-to-Far
순서로 렌더링하는 것이 이상적 - 하지만 Multi Frustum Rendering에서는
Far-to-Near
순서로 렌더링되므로, Early-Z 최적화가 비효율적일 수 있음
해결책: 각 Frustum 내부에서는 Near-to-Far
로 정렬
- 각 개별 Frustum 내에서는
Near-to-Far
로 정렬하여 렌더링하면 Early-Z 최적화를 유지할 수 있음 - 이렇게 하면 GPU가 이미 렌더링된 픽셀을 깊이 테스트에서 제외(Early-Z Reject)할 확률이 높아져 성능이 향상됨
6. Reducing Redundant Computations (연산 최적화)
문제 상황
- 같은 객체를 여러 Frustum에서 렌더링할 경우, 불필요한 연산이 여러 번 실행될 수 있음
- 예를 들어, Shader에서 동일한 계산을 Frustum마다 중복 수행하면 GPU 오버헤드 증가
해결책: 업데이트 로직과 렌더링 로직 분리
- 객체의 변환(Transformation) 및 상태(State) 업데이트는 별도의 업데이트 메서드(Update Method)에서 수행
- 렌더링 단계에서는 가급적 사전 계산된 데이터만 사용하여 불필요한 계산을 줄임
'3D Engine Design for Virtual Globes' 카테고리의 다른 글
컴퓨터에 지구를 위치시키는 법 (feat. Geographic Coordinates) (1) | 2025.03.01 |
---|
- Total
- Today
- Yesterday
- gpu rte
- 병렬 연산
- netwon-rapshon
- sw 마에스트로 15기
- reciprocal approximation
- 3d engine design for virtual globes
- 취업 후기
- 소프트웨어 마에스트로
- topcit 고득점
- 심파이
- 탑싯
- virtual globe
- ear cut
- coordinate transformation
- floating point
- 삼각분할
- 탑싯 후기
- cpu rte
- relative to eye
- Software maestro
- Jittering
- geodetic
- parallel operation
- high-low encoding
- 탑싯 고득점
- gpu rte dsfun90
- 좌표 변환
- 역수 근사
- relative to center
- ear clipping
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |