[Unity] CameraSortingLayerTexture, Full Screen, UV 왜곡

a-a·4일 전

알쓸신잡

목록 보기
32/32

결과물

이번 글은 "파동처럼 보이는 셰이더를 만들었다"는 결과보다, 그 과정에서 내가 무엇을 이해하게 되었는가를 정리하기 위한 글이다.

파동을 일렁이는 타격/피격 효과를 만들고자 했다.
에셋을 사용하면 편하지만, 이번에는 기술적으로 공부하고 직접 적용하면서 이론과 실습을 병행했다.

이번에 공부를 진행하면서, 내가 다루고 있는 것은 단순한 색 변화가 아니라 이미 렌더된 화면을 다시 샘플링해서 왜곡하는 과정이라는 점을 이해하게 됐다. 그리고 그 과정에서 Unity 2D URP의 렌더 파이프라인, CameraSortingLayerTexture, Full Screen 방식과의 차이, 그리고 Shader Graph 노드 하나하나의 의미를 다시 정리하게 됐다.

이 글은 그 흐름을 따라간다.


1. 왜 이 작업을 하게 되었는가

내가 만들고 싶었던 연출은 "특정 위치에서 충격파가 퍼지듯, 뒤 배경이 일그러지는 효과"였다.

겉으로 보면 단순하다.

  • 중심점이 있고
  • 시간이 지나며 반지름이 커지고
  • 그 반지름 부근의 화면이 바깥쪽으로 밀려 보이면 된다

하지만 구현에 들어가 보니 생각보다 많은 질문이 생겼다.

  • 이건 일반적인 포스트 프로세싱인가?
  • 왜 어떤 경우에는 잘 보이고, 어떤 경우에는 전혀 안 보이는가?
  • 왜 파동은 Length로 만들고, 왜곡 방향은 왜 Normalize로 구하는가?
  • 왜 UV를 밀었을 뿐인데 화면이 휘어 보이는가?
  • 왜 정사각형에서는 멀쩡한데 직사각형에서는 찌그러지는가?

결국 이 작업은 "파동 효과 하나 만들기"가 아니라, 화면 좌표와 렌더 순서를 이해하는 작업이 됐다.


2. 처음에는 이렇게 생각했다

처음의 내 생각은 아주 단순했다.

화면 위치를 구해서, 중심점과의 차이를 계산하고, 그걸 적당히 밀면 파동처럼 보이겠지.

이 생각 자체가 완전히 틀린 것은 아니다. 실제로 핵심 구조는 맞다. 다만 이 상태로는 중요한 것이 빠져 있다.

빠져 있던 것 1. 나는 무엇을 왜곡하고 있는가

나는 처음에 "픽셀을 움직인다"고 생각했다. 하지만 실제로 셰이더가 하는 일은 픽셀을 옮기는 것이 아니다.

셰이더는 현재 픽셀에서 어떤 색을 읽어올지 결정할 뿐이다.

즉,

  • 오브젝트가 움직이는 것이 아니라
  • 샘플링 좌표가 바뀌는 것이고
  • 그래서 결과적으로 화면이 밀려 보이는 것이다

이 차이를 이해하지 못하면 UV 왜곡 셰이더를 계속 감각적으로만 다루게 된다.

빠져 있던 것 2. 파동은 한 개의 값으로 만들어지지 않는다

처음에는 중심과의 차이 벡터 하나만 있으면 될 것처럼 느껴졌다. 하지만 실제로는 벡터 하나로는 부족했다.

파동을 만들기 위해서는 최소 두 가지가 분리되어야 한다.

  • 거리: 지금 이 픽셀이 파동 반지름 부근에 있는가
  • 방향: 밀린다면 어느 방향으로 밀려야 하는가

이 둘을 분리하지 않으면 파동을 의도적으로 제어하기 어렵다.


3. Unity 2D URP에서는 이 효과가 어떻게 동작했는가

이번 작업에서 가장 먼저 정리해야 했던 것은, 내가 만든 효과가 전통적인 의미의 fullscreen post process와는 다르다는 점이었다.

3-1. CameraSortingLayerTexture란 무엇인가

Unity 2D URP에서는 2D Renderer Data에서 Camera Sorting Layer Texture 옵션을 켜고, Foremost Sorting Layer를 지정할 수 있다. 그러면 Unity는 뒤쪽 레이어부터 지정한 Sorting Layer까지 렌더된 결과를 텍스처로 준비하고, 그 텍스처를 셰이더에서 사용할 수 있게 해준다.

즉, 내가 읽고 있던 것은 "지금까지 그려진 2D 화면 일부"였다.

이게 중요한 이유는 분명하다.

내 셰이더는 빈 공간을 왜곡하는 것이 아니라,
이미 렌더된 배경을 다시 샘플링해서 왜곡하고 있었기 때문이다.

그래서 효과가 제대로 보이려면 다음 두 가지가 맞아야 했다.

  1. 왜곡하고 싶은 배경이 CameraSortingLayerTexture 안에 실제로 들어 있어야 한다
  2. 왜곡을 그리는 오브젝트는 그 캡처 이후에 렌더되어야 한다

즉, 이 효과는 수식 이전에 Sorting Layer 설계의 영향을 강하게 받는다.

3-2. Full Screen Pass와는 무엇이 다른가

URP의 Full Screen Pass Renderer Feature는 말 그대로 렌더 파이프라인의 특정 시점에 전체 화면을 대상으로 머티리얼을 적용하는 방식이다.

반면 내가 사용한 방식은 다르다.

  • 2D Renderer가 준비한 CameraSortingLayerTexture
  • 특정 Sprite/Material이 샘플링해서
  • 그 부분에서만 화면을 왜곡한다

즉,

  • Full Screen Pass는 "화면 전체에 개입하는 방식"
  • CameraSortingLayerTexture 기반 왜곡은 "오브젝트가 이미 그려진 화면을 참조하는 방식"

이라고 이해하는 편이 훨씬 정확했다.

이 차이를 구분하고 나서야, 왜 어떤 연출은 스프라이트 기반으로 만드는 게 맞고, 어떤 연출은 아예 fullscreen 계열로 설계해야 하는지가 명확해졌다.


4. 파동 셰이더는 실제로 무엇을 계산하는가

이번 작업의 핵심은 아래 한 줄로 요약할 수 있다.

최종 UV = 원본 UV + (방향 벡터 × 세기 스칼라)

즉, 파동 셰이더는 처음부터 복잡한 효과가 아니다. 본질적으로는 다음 세 단계를 거친다.

  1. 현재 픽셀이 중심점에서 얼마나 떨어져 있는지 계산한다
  2. 현재 픽셀이 중심점에서 어느 방향에 있는지 계산한다
  3. 그 방향으로 원본 화면 샘플링 좌표를 조금 밀어 읽는다

이걸 이해하고 나니, Shader Graph 노드들도 제각각 흩어진 기능이 아니라 명확한 역할 분담으로 보이기 시작했다.


5. 내가 사용한 핵심 노드와 각각의 역할

아래는 이번 파동 셰이더를 이해하는 데 핵심이었던 노드들이다.

노드역할내가 이해한 의미
Screen Position현재 픽셀의 화면 좌표지금 이 픽셀이 화면 어디에 있는지 알려주는 출발점
Vector2(center)파동 중심 좌표화면 중앙 또는 클릭 지점 등 기준점
SubtractscreenUV - centerUV중심에서 현재 픽셀로 향하는 상대 벡터
Length상대 벡터의 길이 계산중심에서 얼마나 떨어졌는지 나타내는 거리
Normalize상대 벡터의 길이를 1로 정규화방향만 남긴 단위 벡터
Subtract / Absabs(dist - radius)현재 픽셀이 파동 띠 중심에서 얼마나 벗어났는지 계산
Smoothstep띠를 부드럽게 가공파동 경계를 딱딱하지 않게 만드는 마스크
Multiply방향 × 세기실제로 UV를 어느 방향으로 얼마나 밀지 결정
Adduv + distortion최종 샘플링 좌표 생성
Sample Texture 2D밀린 UV로 텍스처 샘플링왜곡된 화면 결과 출력

이 표만 봐도 이번 셰이더는 크게 두 축으로 나뉜다.

  • 형태를 만드는 축: Length, Abs, Smoothstep
  • 방향을 만드는 축: Normalize

그리고 마지막에 이 둘을 합쳐 UV를 민다.


6. Screen Position에서 시작하는 이유

Screen Position은 현재 픽셀이 화면상 어디에 있는지를 알려준다.

예를 들어 화면 중앙은 대략 (0.5, 0.5)라고 볼 수 있다. 만약 파동 중심도 중앙이라면,

offset = screenUV - centerUV

를 통해 중심과 현재 픽셀 사이의 상대 벡터를 구할 수 있다.

offset은 굉장히 중요한 값이다. 왜냐하면 이 안에 이미 두 가지 정보가 동시에 들어 있기 때문이다.

  • 어느 방향에 있는가
  • 얼마나 떨어져 있는가

예를 들면,

  • 오른쪽 픽셀이라면 offset.x가 양수일 것이다
  • 왼쪽 픽셀이라면 offset.x가 음수일 것이다
  • 중심에서 멀수록 벡터 길이는 커질 것이다

즉, offset은 아직 가공되지 않은 원재료다. 여기서부터 거리와 방향을 따로 뽑아내는 것이 핵심이다.


7. Length는 왜 필요한가

Length는 벡터를 받아 거리 하나로 바꾼다.

dist = length(offset)

이 값의 의미는 단순하다.

현재 픽셀이 파동 중심에서 얼마나 떨어져 있는가

중요한 점은, 이 순간 방향 정보는 사라진다는 것이다.

즉, Length는 벡터를 만드는 노드가 아니라 오히려 벡터를 숫자 하나로 압축하는 노드다.

이 값이 중요한 이유는 파동이 결국 원형 띠이기 때문이다.

파동을 표현하려면 보통 현재 반지름 radius와 현재 픽셀 거리 dist를 비교한다.

  • distradius와 가까우면 파동 띠 위의 픽셀
  • 멀면 띠 밖의 픽셀

그래서 보통 이런 식의 값을 만든다.

waveDelta = abs(dist - radius)

이 값은 "현재 픽셀이 파동 띠 중심에서 얼마나 벗어났는가"를 뜻한다.

즉, Length원형 구조를 만드는 핵심 노드다.


8. Normalize는 왜 필요한가

Normalize는 반대로 방향만 남기는 노드다.

dir = normalize(offset)

이 결과는 길이가 항상 1인 단위 벡터다.

예를 들어,

  • 오른쪽이면 (1, 0)에 가까운 값
  • 왼쪽이면 (-1, 0)
  • 오른쪽 위면 (0.707, 0.707) 정도의 값

이 벡터는 말 그대로 밀 방향을 알려준다.

여기서 중요한 포인트가 있다.

offset 자체도 방향처럼 보일 수는 있지만, 그대로 사용하면 중심에서 먼 픽셀일수록 값이 커진다. 그러면 원하지 않아도 거리와 세기가 같이 엮인다.

하지만 Normalize를 하면 길이가 1로 고정되기 때문에,

  • 방향은 dir
  • 세기는 따로 계산한 마스크 값

으로 분리해서 다룰 수 있다.

이게 제어 측면에서 훨씬 좋다.

즉,

  • Length는 파동의 모양을 만들고
  • Normalize는 파동의 방향을 만든다

라고 이해하면 된다.


9. 파동 띠는 어떻게 만들어졌는가

거리 dist만 있다고 바로 파동이 되는 것은 아니다.

필요한 것은 "특정 반지름 부근에서만 강하게 작동하는 띠"다.

그래서 보통 아래와 같은 흐름을 만든다.

waveDelta = abs(dist - radius)
band = 1 - smoothstep(0, width, waveDelta)

이 값들의 의미를 풀어서 보면 다음과 같다.

abs(dist - radius)

현재 픽셀이 파동 중심 반지름에서 얼마나 떨어졌는지 구한다.

  • 값이 0에 가까우면 파동 띠 중심 근처
  • 값이 클수록 띠에서 멀어진 것

smoothstep

경계를 부드럽게 만든다.

딱 끊기는 원형 띠가 아니라,
중심에서는 강하고 바깥으로 갈수록 부드럽게 약해지는 띠를 만든다.

결과적으로 band는 이런 역할을 한다.

지금 이 픽셀이 얼마나 강하게 왜곡되어야 하는가

즉, band세기 스칼라다.


10. 최종적으로 UV는 어떻게 밀리는가

이제 준비가 끝났다.

  • dir: 어느 방향으로 밀 것인가
  • band: 얼마나 세게 밀 것인가
  • intensity: 전체 강도 조절값

이 세 개를 곱하면 된다.

distortion = dir * band * intensity
finalUV = screenUV + distortion

이 구조를 정확히 이해하는 것이 중요하다.

여기서 finalUV는 "현재 픽셀을 어디에서 읽어올지"를 다시 정한 좌표다.

즉, 파동 셰이더가 실제로 하는 일은 이렇다.

  1. 원래는 screenUV에서 색을 읽어왔어야 할 픽셀이
  2. 이제는 finalUV에서 색을 읽게 된다
  3. 그 결과 배경이 밀려 보인다

즉, 화면이 실제로 움직이는 게 아니라 샘플링 기준점이 움직이는 것이다.

이걸 이해하고 나서야 "UV를 민다"는 표현이 정확히 무슨 뜻인지 체감됐다.


11. 왜 LengthNormalize를 같이 써야 했는가

이번 작업에서 가장 중요하게 정리된 문장은 이것이다.

Length는 파동이 어디서 발생할지를 만들고, Normalize는 그 힘이 어느 방향으로 나갈지를 만든다.

둘 중 하나만으로는 부족하다.

Length만 있으면

원형 띠 자체는 만들 수 있다. 하지만 밀 방향이 없다. 즉, 어디를 기준으로 화면을 밀지 알 수 없다.

Normalize만 있으면

방향은 알 수 있다. 하지만 어디에서만 강하게 왜곡해야 하는지 알 수 없다. 결국 화면 전체가 막연하게 밀릴 수 있다.

그래서 파동 왜곡은 반드시 두 축이 필요하다.

  • 거리 기반 마스크
  • 방향 기반 변위

이번 작업은 이 둘을 분리해서 다루는 연습이었다.


12. 왜 직사각형에서는 찌그러졌는가

이 부분도 이번에 명확히 이해하게 됐다.

screenUV는 0~1 범위라서 얼핏 보면 x, y가 같은 단위처럼 느껴진다. 하지만 실제 화면은 종횡비가 다를 수 있다. 그러면 x축 0.1과 y축 0.1이 화면에서 같은 물리적 길이로 느껴지지 않는다.

그래서 보정 없이 length(offset)를 사용하면,
수학적으로는 원이어도 화면상으로는 타원처럼 보일 수 있다.

이 문제를 해결하려면 보통 거리 계산 전에 aspect 보정을 해준다.

예시 개념은 이렇다.

offset.x *= aspect

그 뒤에 length(offset)를 계산하면 x, y 축 길이 차이를 어느 정도 보정할 수 있다.

즉, 이번에 겪은 찌그러짐 문제는 "파동 수식이 틀린 문제"라기보다,
좌표계를 정규화하지 않고 거리 계산을 했기 때문이라고 보는 편이 맞았다.


이번 작업에서 실제로 겪었던 문제를 다시 해석하면
이번에 겪었던 문제들은 사실 전부 같은 축에서 설명할 수 있었다.

문제 1. 어떤 때는 효과가 안 보이거나 이상하게 보였다

이건 셰이더 수식 문제가 아니라, CameraSortingLayerTexture 안에 내가 왜곡하고 싶은 대상이 제대로 들어 있지 않거나, 효과 오브젝트가 캡처 순서상 잘못된 레이어에 있었기 때문일 가능성이 컸다.

즉, 이 문제는 수학 이전에 렌더 순서 문제였다.

문제 2. 화면 왜곡처럼 보이는데 fullscreen과는 달랐다

이건 Full Screen Pass와 CameraSortingLayerTexture 기반 샘플링을 혼동했기 때문이다.

  • Full Screen Pass는 전체 화면에 개입한다
  • 내가 만든 방식은 특정 오브젝트가 화면 텍스처를 참조한다

이 둘을 분리하고 나서 설계 기준이 훨씬 명확해졌다.

문제 3. 파동은 보이는데 왜 그렇게 보이는지 설명하지 못했다

이건 LengthNormalize를 "노드 기능"으로만 알고 있었지,
각각이 거리와 방향을 분리하는 역할이라는 구조로 이해하지 못했기 때문이었다.

이 부분을 정리하고 나서야 내가 만든 셰이더를 스스로 설명할 수 있게 됐다.


14. 이번 작업을 통해 정리한 기술적 이해

이번 작업을 통해 나는 파동 셰이더를 더 이상 "그럴듯하게 보이는 효과"로 보지 않게 됐다.

이제는 아래처럼 설명할 수 있다.

파동 셰이더는 화면 좌표계에서 중심점과 현재 픽셀의 상대 벡터를 구한 뒤, 그 벡터에서 Length로 거리 스칼라를, Normalize로 방향 벡터를 분리한다. 이후 거리 기반으로 띠 마스크를 만들고, 그 마스크를 방향 벡터에 곱해 샘플링 UV를 밀어줌으로써, 특정 반지름 영역에서만 배경이 바깥으로 휘어 보이게 만든다.

이 문장을 이해하면, 이번에 한 작업의 핵심은 거의 다 이해한 것이다.


15. 마무리

이번 작업은 단순히 셰이더 하나를 만든 경험이 아니었다.

나는 이번 작업을 통해 아래 세 가지를 분리해서 생각하게 됐다.

  1. 렌더 파이프라인 문제: 무엇이 언제 캡처되고 언제 그려지는가
  2. 좌표계 문제: 현재 픽셀과 중심점의 관계를 어떻게 표현하는가
  3. 왜곡 문제: 방향과 세기를 어떻게 분리해서 UV에 반영하는가

이 세 가지가 정리되고 나서야, 그동안 감으로 만지던 노드들이 구조로 보이기 시작했다.

특히 이번 작업에서 가장 크게 정리된 문장은 이것이다.

Length는 파동의 형태를 만들고, Normalize는 파동의 방향을 만든다.

그리고 마지막에,

최종 UV = 원본 UV + (방향 벡터 × 세기 스칼라)

이 한 줄이 파동 왜곡의 본질이라는 것도 정리할 수 있었다.

앞으로 비슷한 셰이더를 만들 때도 먼저 이 질문부터 하게 될 것 같다.

  • 지금 내가 구하는 값은 거리인가, 방향인가?
  • 지금 내가 움직이는 것은 픽셀인가, 샘플링 좌표인가?
  • 이 문제는 수식 문제인가, 렌더 순서 문제인가?

이번 작업은 결과물 하나보다, 그 질문을 할 수 있게 되었다는 점에서 더 의미가 있었다.

profile
게임 개발자란 무엇일까!

1개의 댓글

최고다

답글 달기