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

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

<pname>GLSL 쉐이더를 이용한 클리핑과 왜곡 연출</pname>
<tag>회고,개발</tag>

구현 배경

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

1. 육각형 프레임 클리핑

프로필 이미지가 프레임 밖으로 나가는 문제는, 쉐이더에서 육각형 영역을 수식으로 직접 정의하고 그 바깥은 투명 처리하는 방식으로 해결했다.
핵심은 isInsideHexagon 함수다. 현재 픽셀 좌표를 육각형 중심 기준 상대 좌표로 변환한 뒤, 가로/세로 반지름으로 정규화하고 육각형 판정 수식을 적용한다. 조건을 만족하면 렌더링하고, 그렇지 않으면 투명(vec4(0))으로 처리한다.
uniform으로 center, radius_x, radius_y, alpha를 외부에서 주입하도록 설계해, 프레임 크기나 위치가 달라져도 쉐이더 코드를 수정할 필요가 없다.
// vfx_select_frame.fsh varying vec2 v_vTexcoord; uniform vec2 center; uniform float radius_x; uniform float radius_y; uniform float alpha; bool isInsideHexagon(vec2 point, vec2 center, float radius_x, float radius_y) { vec2 p = point - center; vec2 scaled_p = vec2(p.x / radius_x, p.y / radius_y); float qx = abs(scaled_p.x); float qy = abs(scaled_p.y); return qy <= 1.0 && (1.0 * 0.866 - qx * 0.5 >= qy * 0.5); } void main() { vec2 coord = gl_FragCoord.xy; vec4 tex_color = texture2D(gm_BaseTexture, v_vTexcoord); tex_color.a *= alpha; if (isInsideHexagon(coord, center, radius_x, radius_y)) { gl_FragColor = tex_color; } else { gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); } }
실제 코드
마스크 텍스처를 쓰는 방법도 있지만, 육각형처럼 수식으로 표현 가능한 형태는 수식 기반이 리소스를 아끼고 해상도에도 독립적이어서 유리하다.
별도로, Y 좌표 범위로 클리핑이 필요한 UI 요소(텍스트 등)에는 y_min / y_max uniform으로 범위를 지정하고 범위 밖은 discard하는 단순한 쉐이더(vfx_player_select)도 함께 활용했다.
결과물
결과물

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

프레임 뒤 빛 이미지의 물결 효과는 UV 좌표를 시간에 따라 흔드는 방식으로 구현했다.
시간(time uniform)이 흐르면 각 픽셀이 참조하는 텍스처 위치가 sin/cos에 따라 주기적으로 밀린다. X 방향과 Y 방향에 서로 다른 주파수(0.3, 0.4)와 공간 변수(v_vTexcoord.y * 50, v_vTexcoord.x * 50)를 섞어 단순한 사인파가 아닌 불규칙한 느낌을 냈다.
왜곡된 UV가 [0, 1] 범위를 벗어나면 이미지 경계가 깨지기 때문에 clamp로 범위를 제한했다.
// vfx_profile_background.fsh varying vec2 v_vTexcoord; varying vec4 v_vColour; uniform float time; uniform float threshold; void main() { float offsetX = sin(time * 0.3 + v_vTexcoord.y * 50.0) * 0.001; float offsetY = cos(time * 0.4 + v_vTexcoord.x * 50.0) * 0.001; vec2 coord = clamp( vec2(v_vTexcoord.x + offsetX, v_vTexcoord.y + offsetY), vec2(0.0), vec2(0.9) ); gl_FragColor = v_vColour * texture2D(gm_BaseTexture, coord); }
실제 코드
  • time * 0.3 / 0.4 : X·Y 방향 흔들림 속도 (값이 다르면 더 자연스럽게 보임)
  • 50.0 : 공간 주파수 (클수록 물결이 촘촘함)
  • 0.001 : 진폭 (클수록 왜곡이 강함)
결과물
결과물
 

애니메이션 시트가 아닌 쉐이더를 사용한 이유

같은 "흔들리는 빛" 효과를 미리 렌더링한 스프라이트 시트(프레임 애니메이션) 로도 만들 수 있다. 그럼에도 쉐이더를 택한 이유는 다음과 같다.
  • 메모리 절약 — 여러 프레임의 이미지를 저장할 필요 없이, 이미지 한 장 + 연산으로 처리된다.
  • 완벽한 루프 — 시간 함수 기반이라 끊김 없이 무한히 자연스럽게 반복된다. 프레임 애니메이션 특유의 "이음새"가 없다.
  • 실시간 조절 — 속도·세기·주파수를 파라미터로 노출하면 기획/아트가 값을 바꿔가며 즉시 튜닝할 수 있다. 시트를 다시 뽑을 필요가 없다.
  • 해상도 독립 — 픽셀 단위 연산이라 확대해도 깨지지 않는다.
 

결과 및 회고

  • 룰렛이 회전해도 캐릭터 이미지가 프레임 밖으로 새지 않도록 클리핑을 구현했다.
  • 정적인 빛 이미지가 애니메이션 시트 없이 물결치듯 흔들리는 연출을 완성했다.
  • 두 효과 모두 추가 이미지 리소스 없이 GPU 연산만으로 처리해 메모리를 아꼈다.
  • 같은 연출도 리소스(이미지)로 풀 것인가, 연산(쉐이더)으로 풀 것인가는 명확한 트레이드오프였다. 메모리·유연성·타겟 성능을 저울질하는 감각을 얻었다.
  • UV 조작은 물결 외에도 왜곡·아지랑이·홀로그램 등 다양한 연출로 확장할 수 있는 기본기라는 걸 체감했다.
  • 노이즈 텍스처를 섞으면 단순 sin/cos보다 더 불규칙하고 자연스러운 왜곡을 만들 수 있다.
 
 
<pagelist>