-
[week5] PixelShader - FogUE5 2025. 4. 4. 22:18
학습 목표
- Pixel Shader의 동작을 이해한다.
- 후처리 (Post Process) Pass 구현을 통해 Multi-Pass Rendering에 대해 이해한다.
Fog
- Fog는 Post Process에서 구현할 수 있지만, 제대로 하려면 Main-pass에서 처리하는 게 맞다.
왜 메인 패스에서 Fog를 처리하는가?
1. 픽셀이 “생성될 때” 안개를 적용해야, 물리적으로 맞는 색이 나온다
- 픽셀이 생길 때는:
- 조명 계산
- 머티리얼 색상
- 그림자 여부
- 이때 안개가 존재하면, → 빛의 감쇠, 색 번짐, 환경 색과의 혼합 같은 걸 고려해서 최종 색상을 결정함.
👉 즉, Fog는 그냥 “화면 위에 덮는 투명 레이어”가 아니야.
빛이 대기 속을 통과하면서 감쇠되는 걸 모델링하는 과정이야.
2. 깊이별/고도별 안개량이 오브젝트별로 다르기 때문
- 후처리에서 하면 그냥 Depth 텍스처를 참조해서 "화면 상"에서만 안개 처리 가능
- 그런데 메인 패스에서는 각 픽셀이 실제 어떤 오브젝트에서 왔는지,
어떤 머티리얼을 쓰는지, 그 위치가 월드상 어디인지까지 알고 있어
✔ 즉, 정확한 안개 처리는 픽셀이 그려질 때가 가장 이상적인 시점
❌ 후처리에서 Fog만 쓰면 어떤 문제가 생기냐면…
1. Z 테스트 이후에 덮기 때문에 깊이 정렬에 문제가 생김
→ 예: 앞에 있는 캐릭터도 안개에 가려지거나, 이상한 투명함 생김
2. 라이팅, 그림자 위에 덮어씌우기만 함
→ 물리적으로 맞지 않음 (빛 감쇠 없이 색만 바뀜)
3. 픽셀이 "어디서 왔는지"에 대한 정보 없음
→ 같은 색이라도 가까운 캐릭터랑 먼 산이 같은 안개량 적용될 수 있음
💡 그럼 후처리에서 Fog는 못 써?
쓸 수 있어!
특히 성능이 중요한 모바일 게임이나,
간단한 분위기용 페이크 안개엔 후처리 Fog도 많이 써.PostProcessing에서도 Depth 갖고와서 Fog만들 수 있는데 왜 Main에서 하나?
거리기반으로 안개를 만들어줄 수는 있지만.. 다른 효과는 고려 못한다!
❌ 그런데… "그게 메인 패스랑 완전히 같은 건 아냐"
1. 후처리는 이미 픽셀이 만들어진 후라, 조명/재질/셰이딩이 끝난 상태야
- 후처리는 그냥 색상을 후처리할 뿐,
- 그 픽셀이 왜 그런 색이 되었는지 (예: 머티리얼, 라이팅, 그림자) 정보는 없음
👉 즉, “빛이 안개 속에서 감쇠되었다” 같은 효과를 못 만들어
2. 후처리는 픽셀이 만들어진 ‘이유’를 모름
- 후처리는 그냥 화면에 찍힌 색상을 보고 그 위에 안개 색을 섞는 거야
- 예를 들어, 어떤 픽셀이:
- 먼 배경 오브젝트라서 안개에 가려져야 할 것처럼 보여도,
- 그게 반사라거나, 광원이 닿은 부분이라거나 하면?
- 그냥 덮어씌우는 식의 후처리는 이걸 구분 못 해
✔ 즉, 화면에 있는 색상이 어떤 공간 위치에서 왔는지 정확히 알 수 없음
안개 속에서의 빛 감쇠 효과는 어떻게 일어나는가?
빛이 안개 속을 지나오면서 약해지는 과정을 수식으로 표현하면:
FinalColor = LightColor * SurfaceColor * transmittance(d)
여기서 transmittance(d)는 빛이 거리 d만큼 안개를 통과하면서 얼마나 약해졌는지를 나타내는 값이야.
이걸 계산하려면:
- 빛이 지나온 거리
- 그 경로에 어떤 안개 밀도가 있었는지
- 표면의 위치
- 표면에 닿은 조명 정보
가 필요함
❌ 그런데 후처리에서는?
후처리 셰이더는 그 픽셀이 어떻게 만들어졌는지 모름.
- 예: 어떤 표면에 어떤 조명이 비췄고, 거기까지 빛이 얼마나 감쇠되었는지
- 그걸 표현하려면 빛 → 표면까지의 "빛의 경로", 그리고 표면 → 카메라까지의 "시야 경로" 두 개가 필요해
하지만 후처리는 그저:
float4 color = tex2D(SceneColor, uv); float depth = tex2D(SceneDepth, uv).r;
✔ 이렇게 색과 깊이만 보고 있을 뿐이야.
빛이 어떻게 거기까지 갔는지는 알 수 없어.추가적으로, 메테리얼도 안개를 후처리하면 문제가 생기는데..
💣 문제점 구체적으로 보면:
1. 빛의 감쇠를 무시함
- 진짜 안개는 빛이 약해지는 거야 (transmittance)
- 후처리는 그냥 픽셀 색에 fogColor를 섞는 거라 빛 감쇠 효과가 없음
2. 재질 속성 무시
- 금속, 유리, 나무, 살결… 다 같은 방식으로 안개 처리됨 ❌
- 예: 반사 머티리얼은 실제로 안개 속에서 빛을 더 많이 반사할 수도 있는데 그걸 반영 못함
3. 서로 다른 머티리얼끼리의 안개 표현 불일치
- 반사 오브젝트에 안개가 덜 끼고, 암석 표면엔 진하게 보여야 하는데
- 후처리는 무조건 색만 보고 동일하게 처리함
메인패스 안개는 이렇게
픽셀 셰이더에서 머티리얼/조명/거리/고도/빛 방향 등 모든 정보를 가지고:
float3 litColor = CalculateLighting(worldPos, normal, ...); float transmittance = exp(-fogDensity * distance); float3 finalColor = lerp(fogColor, litColor, transmittance);
→ 이건 빛이 재질을 때리고, 다시 카메라로 오면서 감쇠된 걸 계산하는 진짜 안개 표현
결국에는, Fog를 제대로 구현하기 위해서는 MainPass에서 같이 연산이 일어나야한다!
Fog를 미리 RTV에 구현해서 그 정보를 참조해서 MainPass에서 곱해주던,
MainPass에서 같이 생성해서 연산하던 해야함!
Exponential Fog를 RTV생성 후 샘플링 vs 메인패스에서 바로 계산
🎯 왜 RTV 방식이 더 무거운가?
1. 추가 렌더 패스가 생김
- FogPass()라는 드로우콜이 별도로 하나 더 실행됨
- GPU 입장에선 똑같은 해상도로 전 화면 한번 더 돌리는 셈
2. RTV에 쓰기(write) + **메인 패스에서 읽기(read)**가 둘 다 발생
- Fog Pass에서 FogRTV에 픽셀 쓰고,
- Main Pass에서 다시 FogRTV를 샘플링함
✔ 이건 GPU 메모리 I/O (bandwidth) 비용이 들어
3. 캐시 미스와 텍스처 리소스 바인딩
- 렌더 타겟 교체 (OMSetRenderTargets)는 비용이 작지만 무시 못 할 수준
- FogRTV는 텍스처로 쓰기 → 텍스처로 읽기까지 GPU 캐시 바뀌는 타이밍이 발생
- 이걸 GPU는 리소스 상태 전환(resource barrier)이라고 부르지
4. 동일한 안개 수식도 샘플링 방식으로 바꾸면 간접 비용 발생
// 메인 패스 인라인 float fogFactor = exp(-density * depth); // RTV 방식 float fogFactor = tex2D(FogRTV, uv).r;
- 똑같은 수식이라도 샘플링 비용이 있고, 텍스처 필터링 비용도 따름
- 압축된 포맷일 경우 디코딩 비용까지 생김
✅ 실제 비용 차이 어느 정도?
대부분의 경우, 인라인 계산 방식이 1.5~3배 더 빠르다고 보면 돼
(단순한 안개만 있을 경우)- Exponential 수식 1줄을 픽셀 셰이더에 넣는 건 거의 공짜 수준
- 반면 RTV 방식은 Fullscreen Quad 드로우 + 텍스처 샘플링 → 확실히 더 무거움
안개 RTV를 PostProcessing에서 사용하는 경우에는 안개 RTV를 만드는것이 큰 도움!
📦 정리: 안개 RTV가 필요한 경우 요약표
후처리에서 안개 반영 Tonemap, Bloom, LUT 등 색상/밝기 조절 반사 처리에 안개 적용 SSR, Planar Reflection 반사 씬에도 안개 있어야 함 UI 요소와 자연스럽게 섞기 미니맵, 투명 HUD 등 깊이감 있는 UI 효과 Volumetric Light/God Rays 빛의 퍼짐 연산 시 필요 빛 감쇠량 조정 DOF, MotionBlur에서 연산 보정 흐림 강도 조정 거리감 보정 Deferred Lighting에서 조명 감쇠 계산 조명도 안개 통과함 정확한 조명 표현 Tonemap HDR → LDR 보정 밝기/대비 조정 Bloom 밝은 영역을 번지게 하이라이트 강조 LUT 색상 톤 전체 조정 감성/무드 연출 언리얼에서 Exponential Height Fog, 기본 산란(Basic Inscattering) 안개(Fog)를 구현은 어떻게 되어있나?
Unreal의 BasePassPixelShader.usf 또는 DeferredShadingCommon.ush 안에 보면
ApplyHeightFog() 같은 함수로 처리되고 있어.float FogFactor = ComputeHeightFog(ViewSpacePos, FogParams); FinalColor = lerp(FogColor, LitColor, FogFactor);
postprocessing에서 안개 정보를 사용해야할때도, exponential height fog나 basic inscattering fog와 같이 간단한 안개는
- 계산이 너무 단순하다
- 픽셀당 viewDepth or worldPos.y로 다시 계산 가능하다
- 후처리에서 이 값을 쓰고 싶으면 그냥 다시 계산하면 됨
✅ 정리: RTV가 "필수"인 건 언제인가?
Exponential Fog ❌ 필요 없음 수식 간단, 재계산 가능 Height Fog ❌ 필요 없음 마찬가지 Volumetric Fog ✅ 필요 계산 무거움, 공유 필요 포스트 효과에서 반복 사용 상황에 따라 ✅ SSR, DOF, Bloom 등에서 재사용 씬에서 다른 Pass가 참조해야 할 경우 ✅ 필요 멀티 Pass 공유 모바일에서 Fog Prepass로 처리할 때 ✅ 조건부 필요 퍼포먼스 최적화 목적 'UE5' 카테고리의 다른 글
[week8] Delegate (3) 2025.04.25 [week6] Light (1) 2025.04.12 [week2] Texture Rendering (1) 2025.03.20 [UE5] Development Editor에서 효과적으로 디버깅하는 방법 (1) 2025.01.17 [CG] 레스터라이저 상태(Rasterizer State) (1) 2025.01.15