본문으로 건너뛰기
VFX Diary
VFX Diary
작업기록

UV 왜곡 셰이더 실험: 이동량과 공간 주파수를 따로 바꿔보기

무늬가 심하게 일렁일 때 무엇부터 줄여야 할까요? 읽는 위치가 너무 멀리 움직이는지, 짧은 거리 안에서 방향이 너무 자주 바뀌는지부터 나누어 보고 싶습니다. 이번에는 격자 텍스처 하나를 고정하고 UV 왜곡의 이동량과 공간 주파수를 각각 바꿨습니다.

AI의 도움으로 작성한 코드를 브라우저 WebGL2에서 실제 실행한 기록입니다. 운영자가 Unreal에서 사용해 얻은 경험이나 성능 측정은 포함하지 않습니다. 생성 일러스트 세 장은 입체 재질을 이용한 개념 비유이며, 세 조건을 나란히 둔 비교 화면만 이번 코드의 실제 출력입니다.

직사각형 틀 안에서 격자 무늬가 유기적으로 흐르는 UV 왜곡 개념 일러스트
AI 생성 개념 이미지 — UV로 읽는 무늬의 위치를 바꾸는 발상을 표현한 일러스트이며 실제 실행 캡처가 아닙니다. 입체 재질을 사용한 비유로, 표면의 실제 기하 변형을 뜻하지 않습니다.

같은 격자를 어디서 읽을지 바꾸기

원본은 코드로 만든 256×256 RGBA 텍스처입니다. 가로·세로 여덟 칸의 체크무늬와 얇은 구리색 격자를 넣었습니다. 모든 조건이 같은 바이트를 사용하고, 정면 quad와 카메라, 시드 91, 시간 0초, 512×512 출력도 고정했습니다. 각 화면의 어두운 외곽선은 UV 왜곡 뒤에 별도로 그려 직사각형 경계가 유지되도록 했습니다.

조건 이동량 amplitude 공간 주파수 frequency 기준에서 바꾼 것
기준 0.03 2 비교 출발점
이동량 변경 0.12 2 좌표에 더하는 양만 네 배
주파수 변경 0.03 6 노이즈를 읽는 좌표 범위만 세 배

이동량은 UV 단위이며 월드 공간의 거리나 굴절률이 아닙니다. 주파수는 왜곡을 만드는 노이즈 쪽에만 곱했습니다. 원본 격자의 타일 개수나 텍스처 해상도를 함께 바꾸면 다른 비교가 되므로 그대로 두었습니다.

기준 조건에도 0.03의 이동량이 들어 있습니다. 무왜곡 원본과의 전후 비교가 아니라, 이미 약하게 일렁이는 설정에서 두 조절을 분리한 비교입니다. 왜곡을 켰을 때의 변화 자체를 보려면 amplitude 0을 별도 대조군으로 추가해야 합니다.

동일한 체크 텍스처에서 UV 이동량과 노이즈 공간 주파수를 각각 바꾼 WebGL2 실제 비교 화면
직접 작성한 WebGL2 코드의 실제 출력 — seed91/time0과 같은 checker를 고정했습니다. SwiftShader 소프트웨어 렌더링이며 Unreal Engine이나 GPU 성능 측정 결과가 아닙니다.

실행 환경은 macOS arm64, Playwright 1.62.1, Chromium Headless Shell 151, WebGL2·GLSL ES 3.00입니다. SwiftShader 소프트웨어 렌더러로 실행했고 종료 코드는 0입니다. 같은 입력으로 다시 그린 픽셀은 세 조건 모두 일치했습니다. 이 확인은 이번 세션의 재현성에 대한 것이며, 다른 GPU나 브라우저까지 같은 해시를 보장하지 않습니다.

이동량은 같은 흐름에서 읽는 위치를 더 멀리 옮긴다

가운데 화면에서는 기준보다 격자가 크게 밀리고 기울어집니다. 넓게 휘는 흐름은 남아 있지만 몇몇 사각형의 폭과 기울기가 더 두드러집니다. 외곽 사각형이 휘거나 메시가 변형된 결과는 아닙니다. 각 출력 픽셀이 원본의 어느 위치를 읽을지만 달라졌습니다.

이 차이를 화면의 인상만으로 판단하지 않도록 노이즈 벡터만 색으로 출력하는 진단 모드도 실행했습니다. 기준과 이동량 변경 조건의 RGBA8 노이즈 필드 해시는 같았고, 최종 이미지 해시는 달랐습니다. 이동량 변경이 노이즈 생성 조건을 건드리지 않고 이후 좌표 계산에 적용됐는지 확인한 것입니다. 이 진단에는 성능 측정이 들어 있지 않습니다.

해시는 부동소수점 노이즈 전체가 아니라 8비트 색으로 변환한 진단 출력의 값입니다. 같은 해시를 모든 중간 계산의 정밀도가 같다는 증명으로 확대하지는 않습니다. 입력을 고정한 코드 구조와 진단 출력을 함께 남겨 비교 조건을 확인했습니다.

여기서 네 배라는 표현은 좌표에 더하는 계수의 관계입니다. 화면의 모든 선이 네 배 멀리 보인다거나 왜곡이 네 배 강하게 느껴진다는 뜻은 아닙니다. 격자는 반복되고, 어떤 위치는 같은 색으로 돌아오며, 원본에서 선이 놓인 방향에 따라서도 눈에 보이는 차이가 달라집니다.

같은 격자 무늬가 적게 또는 크게 당겨진 두 고무 타일의 왜곡 세기 개념 이미지
AI 생성 개념 이미지 — 무늬를 읽는 위치의 이동량을 구분하는 비유이며 코드 실험의 수치를 재현하지 않습니다. 입체 재질을 이용한 비유이며 UV만 바꾸는 실행 결과와는 구분됩니다.

주파수는 더 좁은 곳에서 방향이 바뀌게 했다

오른쪽은 이동량 0.03을 유지하고 주파수만 2에서 6으로 바꾼 화면입니다. 기준의 완만한 굴곡보다 짧은 간격의 휘어짐이 더 자주 나타납니다. 노이즈 필드를 색으로 읽은 해시도 기준과 달랐습니다. 반면 원본 텍스처 바이트의 해시는 세 조건에서 같았습니다.

이 결과를 “격자를 세 배 더 타일링했다”고 부르면 무엇을 조절했는지 흐려집니다. 이 코드에서 확대된 것은 노이즈를 조회하는 좌표 범위입니다. 격자 원본은 여전히 여덟 칸이며, 그 원본을 읽을 때 더해지는 오프셋의 공간 변화가 달라졌습니다. 주파수 6은 정확히 여섯 개의 물결이 보인다는 약속도 아닙니다. 사용한 함수는 주기적인 사인파가 아니라 시드가 고정된 value noise입니다.

저는 이 두 조절을 “전체가 얼마나 크게 흐트러지는가”와 “어느 정도 간격으로 꺾이는가”를 살피는 출발점으로 보고 싶습니다. 다만 둘은 눈에 보이는 결과에서 완전히 독립적이지 않습니다. 이동량이 같아도 방향이 더 빨리 바뀌면 이웃한 위치가 서로 다른 곳을 읽으므로 선의 기울기나 늘어짐이 더 강하게 드러날 수 있습니다.

넓은 굴곡과 촘촘한 굴곡이 대비되는 두 정사각형 인레이의 공간 주파수 개념 이미지
AI 생성 개념 이미지 — 왜곡의 변화가 일어나는 공간적 간격을 떠올리기 위한 표현이며 실제 셰이더 비교 결과는 아닙니다.

실행한 코드는 두 배율을 다른 위치에 둔다

핵심 fragment 코드는 아래와 같습니다. noise2는 정수 해시와 부드러운 보간으로 만든 단일 value noise입니다. U와 V에 서로 다른 좌표 오프셋을 사용해 두 성분을 만들고, 0–1 값을 −1–1 범위로 바꿨습니다.

vec2 q = vUv * uFrequency + vec2(uTime * 0.1, 0.0);
vec2 flow = 2.0 * vec2(
    noise2(q + vec2(13.0, 7.0)),
    noise2(q + vec2(41.0, 29.0))
) - 1.0;
vec2 sampleUv = vUv + uAmplitude * flow;
vec3 color = texture(uSource, sampleUv).rgb;

flow는 길이를 1로 정규화한 방향 벡터가 아닙니다. 각 성분이 −1–1 안에 있으므로 각 축에 더하는 양은 amplitude 범위를 넘지 않지만, 모든 위치가 그 최댓값으로 움직이지는 않습니다. amplitude 0.12에서 각 축의 상한이 0.12 UV라는 수식상의 조건과, 캡처에서 실제 선이 이동해 보이는 거리는 구분해야 합니다.

Khronos의 GLSL ES texture 정의는 전달한 좌표에서 텍셀을 읽는 동작을 설명합니다. 여기서도 sampleUv는 출력 정점의 새 위치가 아니라 조회할 원본 위치입니다. 예를 들어 같은 양을 오른쪽으로 더하면 오른쪽의 원본을 읽기 때문에 무늬는 반대 방향으로 밀려 보일 수 있습니다. 이 관계를 놓치면 나중에 흐름 방향을 맞출 때 부호를 두 번 뒤집기 쉽습니다.

다만 반복되는 격자만으로 이동 방향을 추적하기는 어렵습니다. 방향 부호까지 검증할 때는 모서리에 서로 다른 색 표식을 둔 비대칭 원본이 더 적합해 보입니다. 이번 격자는 선의 굴곡을 읽기 위한 선택이며 방향 추적용 표식은 넣지 않았습니다.

가장자리와 선의 거칠음도 실험 조건이다

좌표에 오프셋을 더하면 일부 위치는 0–1 범위를 벗어납니다. 이번 텍스처는 U와 V 모두 REPEAT로 설정했습니다. Khronos의 texture parameter 문서에서 설명하는 반복 주소 방식에 따라 다른 쪽의 무늬를 계속 읽습니다. 가장자리에 빈 색을 더하거나 늘여 붙이는 처리는 넣지 않았습니다.

필터는 확대·축소 모두 NEAREST로 고정하고 밉맵도 만들지 않았습니다. 원본 텍셀 경계가 잘 보이게 하려는 조건입니다. 캡처의 얇은 선에 계단이 보인다고 해서 왜곡 수식 자체가 불연속이라는 결론을 내릴 수는 없습니다. 필터와 출력 해상도를 바꾸면 같은 좌표 계산도 다른 모습으로 읽힐 수 있습니다. 이번에는 필터 비교나 작은 화면에서의 깜빡임을 검증하지 않았습니다.

Unreal로 옮기기 전에 남겨둘 질문

Epic의 Coordinates Material Expressions는 TextureCoordinate의 UV 출력과 U·V tiling을 설명합니다. 이를 참고하면 원본을 읽는 UV와 왜곡용 노이즈를 읽는 UV에 별도 배율을 두는 구조를 생각할 수 있습니다. 같은 TextureCoordinate에 모든 배율을 한꺼번에 곱하면 원본 격자 크기까지 달라져 이번 비교와 조건이 어긋납니다.

현재 코드는 텍스처 색 조회만 바꾸며 Scene Color 굴절, 노멀, World Position Offset, 시간 애니메이션을 구현하지 않았습니다. Unreal Material에 옮긴다면 먼저 원본 크기와 주소 방식, 필터, 노이즈 함수와 색 공간부터 맞춰 보고 싶습니다. 그런 다음 이동량을 고정한 주파수 비교와 주파수를 고정한 이동량 비교를 다시 해야 브라우저에서 관찰한 관계가 유지되는지 판단할 수 있겠습니다.

다음 실험에서는 같은 좌표 계산을 유지한 채 시간을 움직이고 출력 크기도 줄여 보고 싶습니다. 정지 화면에서 읽히는 미세한 굴곡이 움직일 때도 안정적인지는 별개의 질문입니다. 이번 기록은 그 답을 미리 정하지 않고, 어떤 배율이 어느 입력에 작용했는지 다시 확인할 수 있는 기준 화면을 남깁니다.

작은 셰이더 실험 모아보기 · 작업기록 목록

출처 확인일: 2026년 10월 28일. 세 조건은 직접 작성한 WebGL2 코드의 실제 출력이며 개념 일러스트와 구분했습니다. 엔진 재현이나 GPU 성능 향상에 대한 결론은 포함하지 않습니다.

작업 이야기와 댓글

이 글을 보고 떠오른 질문이나 경험을 남겨 주세요. 서로의 시도와 관찰을 나누는 공간입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다