GLSL 쉐이더를 이용한 클리핑과 왜곡 연출

GLSL 쉐이더를 이용한 클리핑과 왜곡 연출

GLSL 쉐이더를 이용한 클리핑과 왜곡 연출

#회고 #게시글 작성중

 
애니메이션 시트 없이, Fragment Shader만으로 룰렛 연출 다듬기

TL;DR

  • 기획 요청은 두 가지였다. ① 룰렛이 돌 때 캐릭터 이미지가 프레임 밖으로 삐져나오지 않게 할 것, ② 룰렛 프레임 뒤의 빛 이미지가 물결처럼 자연스럽게 흔들리게 할 것.
  • ①은 Fragment Shader에서 프레임 형태의 마스킹 영역을 만들어 그 안에서만 렌더링되도록 클리핑해 해결했다.
  • ②는 UV 좌표에 시간 기반 sin/cos offset을 더해, 정적인 이미지 한 장이 애니메이션 시트 없이 물결치듯 왜곡되도록 구현했다.
  • 결과적으로 추가 리소스(스프라이트 시트) 없이 GPU 연산만으로 두 연출을 모두 처리했다.

구현 배경

룰렛 연출에 두 가지 기획 요청이 들어왔다.
첫째, 룰렛이 회전할 때 내부의 캐릭터 이미지가 룰렛 프레임 밖으로 노출되지 않도록 제한해달라는 것. 룰렛이 돌면 이미지가 프레임 경계를 넘어가 보이는 문제가 있었다.
둘째, 룰렛 프레임 뒤쪽의 빛 이미지가 물결처럼 자연스럽게 흔들리는 효과를 넣어달라는 것. 정적인 이미지에 생동감을 주고 싶다는 연출 의도였다.
두 요청 모두 "이미지를 어떻게 보여줄 것인가"에 대한 문제라, Fragment Shader 단계에서 픽셀을 직접 다루는 방식으로 접근했다.

1. 룰렛 프레임 외부 클리핑

회전하는 캐릭터 이미지가 프레임을 벗어나는 문제는, 프레임과 유사한 형태의 마스킹 영역을 쉐이더에서 정의하고, 그 영역 안에서만 렌더링하도록 하여 해결했다.
Fragment Shader에서 현재 픽셀이 마스크 영역 내부인지 판정하고, 바깥이면 그리지 않는(alpha를 0으로 하거나 discard) 방식이다. 이렇게 하면 룰렛이 아무리 회전해도 마스크 밖으로 나간 부분은 화면에 찍히지 않으므로, 캐릭터 이미지가 프레임 밖으로 노출되지 않는다.
아래 코드는 개념을 보여주는 예시다. (실제 마스크 형태·좌표계에 맞게 교체)
// fragment shader (개념 예시) uniform sampler2D uTexture; // 캐릭터 이미지 uniform sampler2D uMask; // 프레임 형태의 마스크 varying vec2 vUV; void main() { vec4 color = texture2D(uTexture, vUV); float mask = texture2D(uMask, vUV).a; // 마스크 영역 내부=1, 외부=0 // 마스크 밖은 그리지 않음 color.a *= mask; gl_FragColor = color; }
마스크는 텍스처로 넣는 방법 외에, 원/사각형 등 수식으로 영역을 정의해 판정할 수도 있다. 프레임 형태가 단순하면 수식 기반이 리소스도 아끼고 해상도에 독립적이어서 유리하다.

2. 이미지 왜곡 (물결 효과)

프레임 뒤 빛 이미지의 물결 효과는 UV 좌표를 시간에 따라 흔드는 방식으로 구현했다.
핵심 아이디어는, 텍스처를 샘플링할 때 사용하는 UV 좌표에 시간 기반의 sin/cos offset을 더하는 것이다. 시간(uTime)이 흐르면 각 픽셀이 참조하는 텍스처 위치가 주기적으로 조금씩 밀리고, 그 결과 정적인 이미지 한 장이 물결치듯 일렁이게 된다.
// fragment shader (개념 예시) uniform sampler2D uTexture; uniform float uTime; varying vec2 vUV; void main() { vec2 uv = vUV; // 시간 기반 sin/cos 로 UV 를 흔들어 왜곡 uv.x += sin(uv.y * 10.0 + uTime * 2.0) * 0.01; uv.y += cos(uv.x * 10.0 + uTime * 2.0) * 0.01; gl_FragColor = texture2D(uTexture, uv); }
  • 10.0 : 물결의 촘촘함(주파수)
  • uTime * 2.0 : 흔들리는 속도
  • 0.01 : 왜곡의 세기(진폭)
이 세 파라미터만 조절하면 잔잔한 일렁임부터 강한 왜곡까지 자유롭게 연출을 튜닝할 수 있다.

트레이드오프: 왜 애니메이션 시트가 아니라 쉐이더였나

같은 "흔들리는 빛" 효과를 미리 렌더링한 스프라이트 시트(프레임 애니메이션) 로도 만들 수 있다. 그럼에도 쉐이더를 택한 이유는 다음과 같다.
  • 메모리 절약 — 여러 프레임의 이미지를 저장할 필요 없이, 이미지 한 장 + 연산으로 처리된다.
  • 완벽한 루프 — 시간 함수 기반이라 끊김 없이 무한히 자연스럽게 반복된다. 프레임 애니메이션 특유의 "이음새"가 없다.
  • 실시간 조절 — 속도·세기·주파수를 파라미터로 노출하면 기획/아트가 값을 바꿔가며 즉시 튜닝할 수 있다. 시트를 다시 뽑을 필요가 없다.
  • 해상도 독립 — 픽셀 단위 연산이라 확대해도 깨지지 않는다.
물론 쉐이더가 항상 정답은 아니다. 매우 복잡한(수식으로 표현하기 어려운) 움직임은 오히려 아티스트가 만든 시트가 낫고, 저사양 타겟에서는 픽셀 연산 부담을 고려해야 한다. 이번 효과는 단순한 주기적 왜곡이었기 때문에 쉐이더 쪽 이점이 명확했다.

결과

  • 룰렛이 회전해도 캐릭터 이미지가 프레임 밖으로 새지 않도록 클리핑을 구현했다.
  • 정적인 빛 이미지가 애니메이션 시트 없이 물결치듯 흔들리는 연출을 완성했다.
  • 두 효과 모두 추가 이미지 리소스 없이 GPU 연산만으로 처리해 메모리를 아꼈다.

회고 / 배운 점

  • 같은 연출도 리소스(이미지)로 풀 것인가, 연산(쉐이더)으로 풀 것인가는 명확한 트레이드오프였다. 메모리·유연성·타겟 성능을 저울질하는 감각을 얻었다.
  • UV 조작은 물결 외에도 왜곡·아지랑이·홀로그램 등 다양한 연출로 확장할 수 있는 기본기라는 걸 체감했다.
  • (심화 방향) 노이즈 텍스처를 섞으면 단순 sin/cos보다 더 불규칙하고 자연스러운 왜곡을 만들 수 있다.
이 글은 결과 GIF/영상이 있으면 설득력이 크게 올라간다. 클리핑 전후, 물결 효과 동작 장면을 짧게 첨부하는 것을 추천.