WebGL - Computational Visuals: 브라우저의 셰이더를 영상의 프레임으로 옮기기

– ArtXTech

WebGL - Computational Visuals : VFX In Codes

브라우저에서는 멀쩡히 움직이던 셰이더가 왜 비디오로 뽑는 순간 검은 화면이 되는가?

2025년의 WebGL 작업은 이 질문에서 시작해 이 질문으로 돌아왔다. 이 질문 하나 때문에 2025년의 나는 WebGL Context, 비디오 텍스처, 프레임 범위, 비동기 로딩을 붙잡고 개같이 굴렀다. 목표는 단순했다. 1분짜리 영상을 넣으면 30fps 기준 1800프레임을 계산하고, 재사용 가능한 GLSL 이펙트를 Remotion에서 호출하는 것.

처음부터 거대한 VFX 시스템을 만들겠다는 계획이 있었던 것은 아니다. 한 줄의 텍스트를 비트에 맞춰 움직이고, 이미지에 조금 더 까리한 질감을 얹고, 브라우저에서 만든 컴포넌트를 영상으로 재사용하고 싶었다. 그런데 frame, fps, texture, uniform, FBO를 하나씩 붙잡다 보니 결국 문제는 효과의 종류가 아니라 렌더링 조건과 시간의 구조라는 사실을 알게 되었다.

이 글은 2025년에 진행한 WebGL·GLSL·Remotion·키네틱 모션 작업을 시간순으로 정리한 기록이다. 완성된 제품 목록이라기보다, 웹 애니메이션을 프레임 기반 영상 파이프라인으로 변환하며 생긴 실험, 부분 구현, 실패 로그, 아키텍처의 변화를 한 편의 VFX 여정으로 묶은 것이다.

이 글에서 구현은 실제 코드와 렌더링 시도가 확인되는 작업, 설계는 코드 예시와 구조가 정리된 작업, 계획은 아직 외부 결과물로 검증되지 않은 구상을 뜻한다. 2025년의 기록은 이 세 층위가 섞여 있기 때문에, 모든 항목을 완성된 프로덕션으로 포장하지 않는다.

처음에는 Three.js보다 twgl을 선택했다. 카메라와 맵핑 설정까지 끌어오는 복잡도를 줄이고 GLSL fragment shader 자체에 집중하기 위해서였다.

작업별 기술 해부와 아이템 목록

작업 무엇을 만들었나 기술 방식 도입 방식 아키텍처 핵심 아이템
첫 Lyrics Visualiser SRT 기반 자막 영상 Remotion, SVG Filter, CSS3 웹 애니메이션을 30fps 프레임으로 변환 parser → subtitle object → frame animation parseSrt, TypewriterSubtitle, SRT
Chroma Drift 리믹스·보컬 커버 기반 MV Logic Pro, Reaper, Python 분석, Remotion, GSAP, WebGL 오디오·자막·이미지·효과를 씬 단위로 조립 VideoStagger, TypoStagger, TextureLayer vocal, MR, SRT, Mondrian Grid, VHS
Video Shader 영상 텍스처에 VFX 적용 twgl.js, GLSL, WebGL Context OffthreadVideo의 프레임을 texture로 연결 MoShaderMain → MoShaderSequence → VideoShader Atkinson, Halftone, Noise, Masking
Video Sequence 비디오 타이밍 안정화 Sequence, OffthreadVideo, staticFile 이미지·비디오 책임 분리 VideoSequencePlayer + MediaInsert start frame, duration, muted, chroma key
MultiVideo Container 여러 영상과 전환 R3F, CSS, clipPath, blend mode 위치·레이어·오디오를 config로 데이터화 container → video items → transitions Side-by-Side, PiP, 2×2 Grid
ShaderToy Port 외부 shader 재구성 GLSL, Three.js/R3F, uniforms 원본의 좌표·시간·텍스처 규칙을 변환 single pass → FBO multipass Bead Pixelator, EigenVapor, Buffer A/B
Motion Catalog 재사용 모션 샘플 GSAP, Remotion, React, TypeScript 짧은 실험을 반복하고 범용/일회성 로직 분류 motion variants → shared components ChaoticFlyIn, stagger, CardFlip, GlitchText

시작점: 다시 손에 GSAP을 쥐다

오래된 웹 애니메이션 감각을 깨우는 일

과거에는 HTML5, CSS3, JavaScript, GSAP으로 배너와 인터랙티브 페이지를 만들었다. 정해진 용량 안에서 스프라이트 시트와 타임라인을 조합하고, 제한된 트래픽 비용 안에서 최대한 유려한 움직임을 뽑아내야 했다. 파일 용량이 곧 비용이던 시절의 작업은 화려하진 않아도, 제한 조건 안에서 무엇을 버리고 무엇을 살릴지 판단하는 훈련이 되었다.

하지만 몇 년 동안 Marketing Tech, CRO, 데이터 분석 중심의 프로젝트를 거치고 대학원으로 돌아오면서 손끝의 모션 감각이 녹슬었다. 2025년에는 GSAP, React, TypeScript, Vite를 다시 연결하며 웹 애니메이션을 처음부터 재학습했다. 이번에는 단순히 브라우저에서 움직이는 것을 넘어, 그 움직임을 영상의 프레임으로 다시 계산할 수 있는가가 기준이었다.

3월의 디버깅 기록에서 중요한 것은 특정 애니메이션을 성공시켰다는 사실보다, 실패를 다루는 방식이 바뀌었다는 점이다.

  • DOM 구조와 CSS Module 선택자부터 검증한다.
  • useRef가 실제 대상에 연결되었는지 확인한다.
  • GSAP 타임라인의 시작·종료·stagger를 로그로 남긴다.
  • 브라우저 화면만 믿지 않고 Cypress 스크린샷과 상태 비교를 사용한다.
  • 코드 변환 전 원본 레이아웃과 시각적 결과를 Source of Truth로 삼는다.

결국 애니메이션 디버깅은 콘솔 에러만 잡는 일이 아니었다. 화면에 아무것도 안 나오는 이유가 CSS인지, ref인지, 타임라인인지, 렌더러 라이프사이클인지 분해하는 일이었다. 눈에 보이는 결과를 다시 데이터와 구조로 번역하는 작업이었다.

첫 번째 Lyrics Visualiser

2025년 1월에 발행한 첫 번째 Lyrics Visualiser는 이 전환의 프로토타입이었다. SVG Filter, CSS3, Scroll Animation과 같은 웹 기술을 Remotion의 30fps 프레임 모델 안으로 옮겼다. SRT 자막은 문자열 목록이 아니라 시작 프레임과 종료 프레임을 가진 데이터가 되었고, TypewriterSubtitle은 그 구간의 진행률에 따라 글자를 출력했다.

SRT
 └─ parseSrt()
     └─ subtitle object
         └─ splitText()
             └─ motion variant
                 └─ frame interpolation
                     └─ Remotion Composition

이 작업은 VFX라 부르기엔 소박했지만, 이후 모든 실험의 핵심 규칙을 만들었다.

모션은 화면에 붙어 있는 장식이 아니라, 시간축 위에서 계산되는 데이터다.

관련 기록: KR-my-first-lyrics-visualiser-mv

프로젝트 파일 구조

VideoEditPlayground/
├── src/remotion/
│   ├── _Shared/
│   │   ├── Helper/
│   │   │   ├── videoFrameCount.ts
│   │   │   └── sequenceManager.ts
│   │   ├── Component/
│   │   │   └── VideoShader.tsx
│   │   └── Effect/
│   │       ├── AtkinsonDithering.tsx
│   │       ├── HalftoneShader.tsx
│   │       └── ...
│   └── _YT/MotionShader/
│       ├── videoImport.ts
│       ├── MoShaderShow.tsx
│       ├── MoShaderSequence.tsx
│       └── MoShaderMain.tsx

렌더링 성공 패턴을 먼저 분리했다

방식 결과
비디오 위에 셰이더 오버레이 성공
비디오 없이 순수 셰이더 출력 성공
비디오를 WebGL 텍스처로 처리 프리뷰 성공, 렌더링 실패

프리뷰에서 보이는 것과 최종 출력이 다르다는 사실이 첫 번째 함정이었다. 몇 시간 동안 fragment shader만 뜯었지만 원인은 셰이더 수식이 아니었다.

검은 화면과 WebGL Context

Remotion은 프레임을 캡처해 비디오로 만든다. 이때 drawing buffer가 보존되지 않으면 프리뷰에서는 보이던 결과가 렌더링 단계에서 사라진다.

const gl = canvas.getContext('webgl', {
  preserveDrawingBuffer: true,
  premultipliedAlpha: false,
  alpha: true
});

preserveDrawingBuffer: true가 첫 번째 해결책이었다. 이 옵션을 찾는 데 3일이 걸렸다. (문서에서 한 줄만 더 크게 써줬으면 내 머리털 몇 가닥은 살아남았을 것이다.)

프레임 범위와 Duration 계산

다음 문제는 Composition의 길이였다.

durationInFrames: 600
frame range: 0-602

비디오 길이 계산이 끝나기 전에 Composition이 임시 프레임 수로 생성되고 있었다. 해결 방법은 영상 메타데이터에서 실제 프레임 수를 먼저 계산한 뒤 Composition을 렌더링하는 것이었다.

useEffect(() => {
  const calculateInitialDuration = async () => {
    const metadata = await getVideoDurationInFrames(CURRENT_VIDEO, 30);
    setTotalDuration(metadata.durationInFrames);
  };

  calculateInitialDuration();
}, []);

if (isCalculating || totalDuration === null) {
  return null;
}

이제 602프레임짜리 영상은 처음부터 602프레임 Composition으로 생성된다.

비디오 텍스처와 비동기 라이프사이클

비디오가 텍스처로 준비되기 전에 셰이더가 실행되면 간헐적인 검은 화면이 나온다. 그래서 delayRender()와 continueRender()를 연결했다.

const VideoShader = ({ videoPath, fragmentShader }) => {
  const [handle] = useState(() => delayRender());

  const onVideoLoad = () => {
    continueRender(handle);
  };

  return (
    <OffthreadVideo
      src={videoPath}
      onLoad={onVideoLoad}
    />
  );
};

최종 데이터 흐름은 다음과 같이 정리했다.

videoImport.ts
  -> MoShaderShow.tsx
      -> getVideoDurationInFrames()
      -> onDurationCalculated()
          -> MoShaderSequence.tsx
              -> MoShaderMain.tsx
                  -> Composition

OffthreadVideo
  -> VideoShader.tsx
      -> WebGL Context
          -> AtkinsonDithering.tsx

이 구조 덕분에 비디오 길이, 시퀀스 길이, Composition 길이가 같은 프레임 수를 공유하게 되었다.

무엇을 어디에서 가져왔는가

2025년 작업은 완전히 무에서 만들어진 것이 아니었다. 다만 외부 코드를 그대로 복사한 것도 아니었다. 각각의 소스에서 표현 방식, 실행 구조, 문제 해결 패턴을 가져오고, Remotion의 프레임 렌더링 환경에 맞게 다시 설계했다.

참조한 것 가져온 컨텍스트 내 작업에서의 재해석
과거 GSAP 배너 작업 작은 용량 안에서 타임라인을 설계하는 감각 브라우저 프리뷰와 모션 프로토타이핑
React/Vite 생태계 컴포넌트·타입·모듈 기반 구조 웹 애니메이션을 영상 컴포넌트로 분리
ShaderToy 커뮤니티 fragment shader, iTime, iChannel, procedural visual WebGL uniform과 Remotion frame으로 변환
React Bits 및 shader background 예시 효과 조합과 시각적 패턴 직접 제어 가능한 shader preset과 카탈로그
TouchDesigner displacement, chromatic aberration의 빠른 프로토타이핑 짧은 footage와 효과 레퍼런스 생성
Figma, Jitter, Cavalry 스토리보드와 레이아웃·모션의 시각적 검증 최종 구현 전 moodboard와 motion catalog 작성
After Effects/Bodymovin 컴포지션, 레이어, 키프레임, Time Remap Remotion의 시간 계산과 GSAP의 DOM 적용으로 재구성
Vanilla JS 멀티패스 예제 핑퐁 버퍼와 Buffer A/B/Image 흐름 R3F의 독립 Scene, Mesh, FBO 구조

가져온 것은 코드보다 사고방식에 가까웠다. 브라우저용 효과를 영상에 그대로 붙일 수 없다는 사실을 인정하고, 각 환경의 시간과 생명주기를 다시 번역했다.

WebGL·Remotion·MV 프리뷰

CSS 여러 레이어와 글리치 이펙트를 Remotion 프리뷰 타임라인에 맞춰 확인한 화면.

GSAP, Remotion, React/TypeScript로 만든 파편적인 컴포넌트를 한 타임라인에 맞춰 여러 샘플로 시연한 프리뷰.

Lyrics MV에 사용하기 위해 샘플링한 WebGL shader background.

ShaderToy에서 영감을 받은 텍스처 효과를 분리하고 2×2 카탈로그로 비교한 화면.

키네틱 모션 샘플

Title Motion 샘플. 텍스트를 단순한 문자열이 아니라 움직임의 단위로 다루기 시작한 시점의 기록.

AnimatedList, BounceText, BubblePopText, CardFlip, FloatingBubbleText, GeometricPatterns 등 모션 패턴 카탈로그.

후속 도구로 이어진 이미지

후대의 Texture Mix 화면. 이미지를 입력하고 여러 vertex/fragment 효과를 레이어로 조합하며, 노이즈와 VHS 질감을 비교하는 방향으로 발전했다.

Background Maker 계열의 프리뷰. GUI에서 파라미터를 확인한 뒤 Remotion에서 schema로 재생성하는 발상.

Labs로 넘어가기: 왜 Texture Mix가 필요했나

WebGL 효과를 영상에 적용하면서 반복해서 같은 마찰을 만났다.

  1. 브라우저 프리뷰와 Remotion 렌더링 결과가 다르다.
  2. 60fps와 30fps의 시간 모델이 다르다.
  3. 비디오·이미지·텍스처 로딩 순서가 다르다.
  4. 한 장면에 여러 효과를 얹으려면 shader 코드와 props를 계속 수정해야 한다.
  5. 마음에 드는 조합을 발견해도 그 값을 다른 장면에 옮길 방법이 없다.
  6. 매번 영상을 export하고 다시 편집툴로 가져오면 오디오 싱크와 비교 큐레이션에 friction이 생긴다.

그래서 다음 단계에서는 효과를 “한 번 렌더링해서 끝나는 필터”가 아니라, 입력 이미지와 파라미터를 바꾸며 조합을 탐색할 수 있는 제작 인터페이스로 보기 시작했다.

Texture Mix의 기본 발상은 2025년 5월 20일 작성된 자체 제작툴 문서에 처음 정리되어 있다. 다만 현재 문서에 포함된 일부 화면과 세부 기능은 후대에 보완된 회고 자료이므로, 2025년에는 완성된 상용 툴이 아니라 다음 요구사항이 형성된 단계로 이해하는 것이 정확하다.

  • 여러 micro-fragment effect를 레이어처럼 조합한다.
  • vertex와 fragment의 파라미터를 props와 schema로 관리한다.
  • GUI 프리뷰에서 효과 조합을 비교한다.
  • 선택한 값을 Remotion Composition의 명세로 옮긴다.
  • 동일한 효과를 이미지·비디오 블록과 오디오 타임라인에 재사용한다.
graph TD
    A[Input Image or Video] --> B[Texture Mix Preview]
    B --> C[Vertex and Fragment Layers]
    C --> D[Parameter Schema]
    D --> E[Remotion Composition]
    E --> F[Audio and Subtitle Timeline]
    F --> G[Deterministic Video Render]

즉 Texture Mix는 WebGL 실험과 분리된 별도 발명품이 아니다. preserveDrawingBuffer, duration 계산, uniform 설계, FBO 패스, 2×2 프리뷰 카탈로그, 컴포넌트 모듈화가 모두 축적된 뒤에 나온 자연스러운 다음 질문이다.

매번 shader 코드를 고치는 대신, 효과를 조합하고 비교하고 다시 호출할 수는 없을까?

이 질문이 [Labs — Personal Production Tools]라는 다음 아티클로 이어진다.

관련 기록: KR-my-own-production-tool-texture-mix
관련 기록: KR-my-own-production-tool-bg-maker

Computational Visuals를 만드는 방식

셰이더는 하나의 거대한 효과가 아니라 작은 fragment effect의 조합으로 다뤘다.

  • Atkinson Dithering
  • Halftone
  • Noise
  • VHS Texture
  • Glitch Color
  • Pixel Distortion
  • Masking

각 효과의 파라미터를 props로 노출하고, 영상이나 이미지 블록에 적용할 수 있게 만들었다. 이 방식은 이후 자체 제작툴 Texture Mix와 Background Maker로 이어졌다.

KR-my-own-production-tool-bg-maker KR-my-own-production-tool-texture-mix

Lyrics MV에 기여한 것

WebGL 실험은 나중에 Lyrics MV의 배경 장식이 된 것이 아니다. 비주얼의 시간 구조 자체를 바꿨다.

Chroma Drift와 Niji에서는 다음 요소들이 오디오·자막 타임라인과 같은 프레임 축에서 움직였다.

  • SRT 자막의 시작·종료 프레임
  • 보컬 에너지 피크
  • 이미지 마스크
  • CSS3 Blending Mode
  • 디더링과 글리치 컬러셋
  • 텍스처의 강도와 왜곡값

브라우저에서 보이는 WebGL과 Remotion에서 추출되는 WebGL은 다른 환경이다. 그래서 셰이더를 그대로 가져오는 것이 아니라 렌더링 조건, FPS, 비디오 로딩, 버퍼 보존을 고려해 다시 아키텍처링해야 했다.

이 경험은 Niji의 디스토피아적 텍스처와 몬드리안 그리드 파열, Chroma Drift의 VHS 질감, 그리고 이후 Video Stagger와 Freeze Frame 실험의 기반이 되었다.

KR-Lyrics-MV-Projects KR-the-complete-lyrics-visualiser-mv-niji KR-the-second-version-trial-lyrics-visualiser-chroma-drift


한눈에 보는 2025년의 발전 과정

시기 작업 축 기술 방식 도입 방식 아키텍처의 변화 상태
6월 Video Shader Pipeline WebGL, GLSL, twgl.js, OffthreadVideo 비디오를 텍스처로 읽고 fragment shader를 적용 VideoShader와 공용 Effect 계층 구축 구현·해결 과정
7월 비디오 시퀀스·컨테이너 Sequence, OffthreadVideo, mixBlendMode, chroma key 비디오와 이미지를 별도 컴포넌트로 분리하고 데이터로 배치 VideoSequencePlayer와 다중 비디오 컨테이너 도입 구현·설계 혼재
8월 ShaderToy 포팅·멀티패스 Three.js/R3F, useFrame, useFBO, GLSL, FBO 단일 패스에서 Buffer A/B/Image 구조로 확장 효과 컴포넌트와 멀티패스 렌더러를 분리 구현·실험
9–11월 카탈로그와 파이프라인 재평가 Figma, Jitter, Cavalry, TouchDesigner, Remotion 효과를 장면 단위가 아니라 재사용 가능한 샘플·토큰으로 정리 “작품”에서 “제작 시스템”으로 관심 이동 회고·정리

2025년 5월: 코드 기반 자막 캡션을 적용한 자막 애니메이션

레이어 담당 대표 기술
Input 자막, 이미지, 비디오, 오디오 이벤트 SRT, JSON, CSV
Parser 타임코드와 텍스트 토큰화 parseSrt(), splitText()
Timekeeper 현재 프레임과 진행률 계산 Remotion, useCurrentFrame(), interpolate()
Executor 브라우저 프리뷰용 DOM 적용 GSAP, gsap.set()
Skin 정적 레이아웃과 시각 스타일 CSS3, CSS custom properties
Renderer 최종 프레임 출력 Remotion Composition

6월 602프레임: 시간의 주인을 다시 정하다

dance.mov를 연결하면서 20.02초 영상이 602프레임이라는 사실을 확인했다. 하지만 Composition은 먼저 기본값 600프레임으로 만들어졌고, 실제 렌더러는 0–602 범위를 요청했다.

Composition durationInFrames: 600
requested frame range: 0–602

해결은 상위 구조에서 영상의 실제 길이를 먼저 계산하는 것이었다.

useEffect(() => {
  const calculateInitialDuration = async () => {
    const metadata = await getVideoDurationInFrames(CURRENT_VIDEO, 30);
    setTotalDuration(metadata.durationInFrames);
  };

  calculateInitialDuration();
}, []);

if (isCalculating || totalDuration === null) {
  return null;
}

데이터 흐름은 videoImport.ts에서 시작해 MoShaderShow.tsx가 duration을 계산하고, MoShaderSequence.tsx와 MoShaderMain.tsx가 같은 프레임 수를 공유하는 방식으로 정리했다.

videoImport.ts
  -> MoShaderShow.tsx
      -> getVideoDurationInFrames()
          -> MoShaderSequence.tsx
              -> MoShaderMain.tsx
                  -> Composition

여기에 비디오 텍스처 로딩을 기다리는 delayRender()와 continueRender()를 결합했다. 텍스처가 준비되기 전에 shader가 실행되면 간헐적으로 검은 화면이 생겼기 때문이다.

const [handle] = useState(() => delayRender());

const onVideoLoad = () => {
  continueRender(handle);
};

이 시점부터 VFX는 단순한 GLSL 코드가 아니었다. 비디오 메타데이터, 비동기 로딩, Canvas Context, 프레임 범위, 캡처 버퍼를 함께 설계해야 하는 렌더링 아키텍처가 되었다.

8월: ShaderToy를 그대로 복사하지 않고 실행 환경까지 포팅하기

6월의 twgl.js 기반 단일 셰이더 실험 이후, 8월에는 ShaderToy 커뮤니티에서 본 효과를 Three.js와 React Three Fiber 방식으로 옮기는 작업을 진행했다. 정확한 전환 시점은 기록마다 다르게 보이므로, 여기서는 “6월에는 twgl 기반 비디오 셰이더를 시도했고, 8월 문서에서는 Three.js/R3F 멀티패스 구조를 정리했다”고 구분한다.

단일 패스: Bead Pixelator

Bead Pixelator 같은 효과는 ShaderToy의 코드를 WebGL/Three.js GLSL에 맞게 변환하는 입문 단계였다.

ShaderToy WebGL / Three.js
mainImage(out vec4 fragColor, in vec2 fragCoord) void main()
fragColor gl_FragColor
fragCoord / iResolution vUv 또는 vUv * resolution
iTime uniform float time
iChannel0 uniform sampler2D tDiffuse
암묵적 정밀도 precision highp float;

코드만 바꾸는 것은 생각보다 간단했다. 진짜 문제는 원본이 기대하는 카메라, 씬, 좌표계, 텍스처 방향, 해상도, 시간 단위를 함께 옮기는 일이었다. ShaderToy의 결과를 재현하려면 shader 문자열만 가져와서는 안 된다. 실행 환경 전체를 번역해야 한다.

멀티패스: EigenVapor와 검은 공포

EigenVapor는 단일 패스 래퍼로 감당할 수 있는 범위를 넘어섰다. 원본 ShaderToy의 데이터 흐름은 다음과 같았다.

Video / iChannel0
       │
       ▼
Buffer A ─────┐
               ├──> Image Pass ───> Screen
Buffer B ─────┘

이를 Three.js/R3F 안에서 구현하기 위해 useFBO()로 중간 결과를 저장하고, useFrame() 안에서 렌더링 순서를 명시적으로 제어했다.

graph LR
    A[OffthreadVideo] --> B[videoTexture]
    B --> C[Buffer A Pass]
    C --> D[FBO A]
    E[Procedural Shader] --> F[Buffer B Pass]
    F --> G[FBO B]
    D --> H[Final Image Pass]
    G --> H
    H --> I[Screen]

처음에는 useFrame, useFBO를 Canvas 바깥에서 호출하거나, 여러 패스를 하나의 씬과 메쉬로 해결하려 했다. 당연히 React 훅 규칙 위반, program not valid, feedback loop, 누락된 uniform, 텍스처 경계 오류가 연달아 발생했다.

해결책은 구조를 늘리는 것이었다. (코드를 줄이면 우아해 보일 줄 알았는데, 이 경우에는 반대였다.)

  • 멀티패스 렌더러와 개별 효과 컴포넌트를 분리한다.
  • 각 Pass가 독립적인 Scene과 Mesh를 갖게 한다.
  • FBO의 형식과 필터를 명시한다.
  • texture sampling 전에 좌표를 clamp한다.
  • Buffer A/B/Image의 입력과 출력 관계를 코드로 드러낸다.
  • 중간 패스의 결과를 화면에 직접 그려볼 수 있는 디버그 모드를 둔다.

이 작업을 통해 만든 핵심 구조는 다음과 같다.

EigenVaporEffect
  └─ MultiPassRenderer
      ├─ RendererCore
      │   ├─ FBO A
      │   ├─ FBO B
      │   └─ Context Provider
      ├─ Pass 0: video -> Buffer A
      ├─ Pass 1: procedural -> Buffer B
      └─ Final Pass: A + B -> Image

이때 가장 크게 배운 것은 다음 한 문장이다.

GLSL 포팅은 코드 변환이 아니라 실행 조건의 번역이다.

관련 기록: 2025-08-20-remotion-shadertoy-multipass-effect-from-mono-video-handling-to-container-pattern-kr

한눈에 보는 2026년의 발전 과정

시기 작업 축 기술 방식 도입 방식 아키텍처의 변화 상태
1월 첫 번째 Lyrics Visualiser SVG Filter, CSS3, Scroll Animation, Remotion 기존 웹 애니메이션 감각을 30fps 프레임 구조로 변환 텍스트와 SRT를 프레임 이벤트로 연결 프로토타입·완료
3월 Hero/Gallery 디버깅 React, Vite, SCSS Module, GSAP, ScrollTrigger, Cypress 레퍼런스 DOM과 애니메이션을 분해하고 선택자·레이아웃·타임라인을 검증 코드 생성보다 분석 → 설계 → 검증 → 구현 프로세스 확립 실험·설계
5월 Chroma Drift와 모션 컴포넌트 GSAP, Remotion, TypeScript, SRT, useCurrentFrame() 기존 컴포넌트를 MV용 시퀀스로 재조립 GSAP과 Remotion의 책임을 분리하고 _Shared 모듈화 시작 구현·리팩토링

기술 아키텍처: 한 줄의 가사에서 한 프레임으로

graph TD
    A[SRT JSON CSV] --> B[Parser and Normalizer]
    B --> C[Subtitle and Motion Schema]
    C --> D[Frame Math]
    D --> E[Kinetic Typography]
    D --> F[Texture and Shader Events]
    E --> G[Remotion Composition]
    F --> G
    G --> H[Frame by Frame Video]

2026년 5월: Chroma Drift와 코드 기반 키네틱 모션

5월에는 하나의 MV를 만드는 것과 동시에, 다음 MV에서도 꺼내 쓸 수 있는 모션 컴포넌트를 만들기 시작했다. 5월 14일에는 약 38KB까지 커진 motionVariants.ts의 과부하와 GSAP·Remotion 이중 엔진 문제를 분석했다. 5월 23일에는 Chroma Drift 보컬 커버와 타이포그래피 프로그레스를 진행했고, 27일에는 첫 씬을 마무리한 뒤 Niji 씬으로 넘어갔다. 28일에는 Hero Scroll, Jitter 결과물, VideoStagger를 정리했고, 29일에는 실패한 ChaoticFlip을 수정해 ChaoticFlyIn으로 확장했다.

여기서 얻은 결론은 전면 교체가 아니었다.

  • GSAP은 브라우저에서 빠르게 아이디어를 검증하는 엔진으로 유지한다.
  • Remotion은 frame, fps, interpolate()를 이용해 영상의 시간을 계산한다.
  • CSS는 폰트, 색상, 레이아웃처럼 정적인 스타일을 맡긴다.
  • 반복되는 자막, stagger, caption, shader는 _Shared로 이동한다.

같은 파일 안에 GSAP의 back.out(1.7)과 Remotion의 interpolate()가 섞이면 프리뷰에서는 그럴듯해도 렌더링 결과가 어긋났다. 브라우저의 requestAnimationFrame과 Remotion의 프레임 단위 렌더링은 시간의 주인이 다르기 때문이다. 그래서 다음과 같은 책임 분리를 채택했다.

Chroma Drift의 주요 모션 아이템은 다음과 같았다.

  • VideoStagger: 비디오 블록이 시간차를 두고 등장하는 패턴
  • TypoStagger: 글자 또는 단어 단위의 순차 등장
  • HeroScroll: 화면 전체의 이동과 전환
  • ChaoticFlyIn: 난수 기반의 비정형 진입 모션
  • Mondrian Grid: 화면을 분할하는 직교형 마스크
  • Bento Grid: 이미지와 텍스트 블록을 카드형으로 배치
  • CSS3 Blend Layer: 색수차, 글리치, VHS 질감의 레이어 합성

이때의 작업은 나중에 말할 Texture Mix의 요구사항을 이미 품고 있었다. 하나의 장면을 위해 만든 효과가 아니라, 여러 이미지와 비디오에 다시 적용할 수 있는 파라미터화된 질감 레이어가 필요해지고 있었다.

관련 기록: KR-the-second-version-trial-lyrics-visualiser-chroma-drift

2026년 6월: WebGL 셰이더를 Remotion에 넣다

6월 15일의 작업 목표는 명확했다.

1분짜리 영상을 넣으면 30fps 기준 약 1,800프레임을 계산하고, 재사용 가능한 GLSL 효과를 import 한 번으로 적용한다.

처음에는 Three.js보다 twgl.js를 선택했다. 카메라, 씬, 메쉬, 머티리얼을 모두 끌어오는 대신, fragment shader와 WebGL Context에 집중하고 싶었기 때문이다. 프로젝트의 초기 구조는 다음과 같았다.

VideoEditPlayground/
└── src/remotion/
    ├── _Shared/
    │   ├── Helper/
    │   │   ├── videoFrameCount.ts
    │   │   └── sequenceManager.ts
    │   ├── Component/
    │   │   └── VideoShader.tsx
    │   └── Effect/
    │       ├── AtkinsonDithering.tsx
    │       ├── HalftoneShader.tsx
    │       └── ...
    └── _YT/MotionShader/
        ├── videoImport.ts
        ├── MoShaderShow.tsx
        ├── MoShaderSequence.tsx
        └── MoShaderMain.tsx

프리뷰와 렌더링은 다른 생물이었다

처음에 확인한 결과는 다음과 같았다.

방식 프리뷰 최종 렌더링
비디오 위에 셰이더 오버레이 성공 성공
비디오 없는 순수 프로시저럴 셰이더 성공 성공
비디오를 WebGL 텍스처로 읽어 처리 성공처럼 보임 검은 화면

여기서 몇 시간을 fragment shader 수식만 뜯어고쳤다. 그런데 문제는 수식이 아니었다. Remotion은 프레임을 캡처하는 과정에서 WebGL drawing buffer가 보존되지 않으면 프리뷰에서 보이던 결과를 다음 단계에서 잃어버렸다.

const gl = canvas.getContext('webgl', {
  preserveDrawingBuffer: true,
  premultipliedAlpha: false,
  alpha: true
});

preserveDrawingBuffer: true는 화려한 옵션이 아니지만, 이 파이프라인에서는 핵심 조건이었다. 검은 화면은 아무런 진단 정보도 주지 않는다. 셰이더가 틀렸는지, 텍스처가 비어 있는지, Canvas가 사라졌는지, 비디오가 아직 로드되지 않았는지 전부 똑같이 검게 보인다. (검은 화면은 디버깅 UI로는 최악의 UX다.)

2026년 6월에 만든 효과의 단위

효과는 하나의 거대한 shader가 아니라 작은 fragment effect의 조합으로 다뤘다.

  • Atkinson Dithering
  • Halftone
  • Noise
  • VHS Texture
  • Glitch Color
  • Pixel Distortion
  • Masking
  • Chromatic Aberration

각 효과는 intensity, speed, position, displacement 같은 값을 props와 uniform으로 노출하고, 이미지·비디오 블록에 적용할 수 있도록 만들었다. 이 작은 분리 단위가 이후 멀티패스와 자체 제작툴의 기초가 되었다.

관련 기록: KR-remotion-and-shader-converting-glsl-to-webgl

7월: 효과보다 먼저 컨테이너를 만들다

셰이더가 안정되기 시작하자 영상 레이어 자체가 문제로 떠올랐다. 기존 MediaInsert는 이미지와 비디오를 한 컴포넌트 안에서 함께 처리하고 있었다. 그 결과 비디오가 여러 번 메모리에 로드되고, 프레임 타이밍과 오디오 제어가 꼬이고, Remotion Timeline에서 어떤 비디오가 어느 구간을 차지하는지 한눈에 보이지 않았다.

그래서 책임을 분리했다.

VideoSequencePlayer
 └─ Sequence
     └─ VideoRenderer
         ├─ OffthreadVideo
         ├─ ChromaKeyVideo
         └─ per-video style / audio / timing

MediaInsert
 └─ image, text, static media only

각 비디오는 독립적인 Sequence 안에서 from, durationInFrames, startFrom, muted, zIndex, mixBlendMode를 갖게 했다. 이미지와 비디오는 서로 다른 생명주기를 가지므로 같은 인터페이스로 억지로 묶지 않는 편이 오히려 재사용성이 높았다.

그 다음 단계가 MultiVideoPlayer와 컨테이너 패턴이었다.

  • Side-by-Side
  • Picture-in-Picture
  • 2×2 Grid
  • Clip Mask
  • Chroma Key
  • Fade, Slide, Flip, Wipe, Iris, Cube Transition
  • 비디오별 volume, playback rate, start frame

핵심은 화면을 직접 하드코딩하는 대신, 설정 데이터를 통해 배치하는 것이었다.

interface MultiVideoItem {
  id: string;
  src: string;
  position: {
    x: number;
    y: number;
    width: number;
    height: number;
  };
  startFrame: number;
  duration: number;
  zIndex?: number;
  mixBlendMode?: string;
}

이 구조는 단순히 영상을 여러 개 보여주는 기능이 아니었다. 레이어, 마스크, 오디오, 전환, 타이밍을 하나의 선언적 스키마로 옮기는 과정이었다. 나중에 셰이더 파라미터를 schema로 추출해 Remotion에 전달하려 했던 이유도 여기서 나왔다.

관련 기록: 2025-07-06-video-framing-refactoring.md, 2025-07-11-refactoring.md


에필로그: 셰이더보다 중요한 것은 맥락이다

WebGL을 배우며 알게 된 건 GLSL 문법만이 아니었다. 같은 코드라도 브라우저 프리뷰와 프레임 기반 비디오 렌더링에서는 다르게 작동한다는 사실이었다.

결국 Computational Visuals는 효과를 많이 쌓는 기술이 아니다. 오디오, 프레임, 텍스처, 메모리, 출력 포맷의 제약을 이해하고 그 안에서 원하는 감정을 보존하는 일이다.

  • 웹의 requestAnimationFrame을 영상의 deterministic frame으로
  • ShaderToy의 iTime과 iChannel을 Remotion uniform과 texture로
  • 비디오 메타데이터를 Composition duration으로
  • SRT 자막을 글자 단위의 모션 이벤트로
  • TouchDesigner의 실시간 효과를 재사용 가능한 footage와 shader preset으로
  • Figma의 레이아웃과 moodboard를 데이터 기반 컴포넌트로
  • 실패한 렌더링 로그를 다음 아키텍처의 규칙으로

그래서 이 여정의 핵심 결과는 특정 VFX 하나가 아니다. 브라우저, 영상, 셰이더, 오디오, 자막, 디자인 시스템 사이를 오가며 같은 시각적 의도를 다른 실행 환경에 맞게 재구성하는 번역 능력이다.

이제 다음 이야기는 효과를 만드는 데서 멈추지 않는다. 반복되는 shader layer를 어떻게 프리뷰하고, 파라미터를 어떻게 저장하고, Remotion 영상 파이프라인에 어떻게 다시 주입할 것인가. 그 질문이 [Labs — Personal Production Tools]로 이어지고, Texture Mix에서 다시 [Lyrics MV Projects]의 파일 구조와 씬 오케스트레이션으로 이어진다. 마지막에는 각각 따로 보였던 오디오, 모션, WebGL, 타이포그래피 실험들이 하나의 제작 방식으로 수렴한다.

VFX In Codes
   ↓
Labs — Personal Production Tools
   ↓
Lyrics MV Projects
   ↓
All Experiments Combined Into Making MVs

셰이더를 작성하는 사람은 많다. 하지만 어떤 장면에 어떤 파라미터를 배치하고, 왜 그 프레임에서 그 효과가 필요한지 결정하는 사람은 따로 있다. 현장의 제약 조건을 이해하고 맥락을 통제하는 도메인 전문가, 인간 디렉터의 가치가 바로 그 지점에서 드러난다.


Special Section: “셰이더 하나 짜면 영상이 뚝딱 나온다”는 신화

요즘은 AI에게 Remotion 코드를 시키고, MCP나 skills를 연결하면 몇 분 만에 복잡한 모션 그래픽을 만들 수 있다는 식의 이야기가 자주 나온다. 단순한 카드 애니메이션이나 발표자료 수준의 장면이라면 가능할 수 있다. 하지만 3분 이상의 영상에서 여러 커스텀 모션, 동기화된 자막, 오디오 피크, 마스크, 텍스처, 전환을 동시에 제어하려면 이야기가 달라진다.

AI가 만들어주는 코드는 대체로 다음 질문에 답하지 못한다.

  • 이 효과는 브라우저 프리뷰용인가, 헤드리스 렌더링용인가?
  • 현재 시간의 기준은 requestAnimationFrame인가, Remotion frame인가?
  • 비디오 텍스처가 준비되기 전에 shader가 실행되면 어떻게 되는가?
  • 어느 레이어가 audio를 가져야 하는가?
  • 이 값을 다음 MV에서도 재사용할 수 있는 schema인가?
  • 검은 화면이 나왔을 때 문제의 위치를 어떻게 격리할 것인가?

결국 필요한 것은 프롬프트보다 범위, 제약, 데이터 흐름, 렌더링 조건을 문서화하는 능력이다. 그 문서를 쓰고 나면 가끔 이런 결론에 도착한다. (이렇게까지 명세를 써서 AI에게 설명할 바엔 그냥 내가 직접 짜는 게 더 빠른데?)

그렇다고 AI를 쓰지 않는다는 뜻은 아니다. 코드 변환, 반복적인 타입 정의, shader boilerplate, 문서 구조화에는 유용하다. 다만 최종 판단은 인간 디렉터가 해야 한다. 어떤 순간에 RGB 분리를 넣고, 어느 프레임에서 글리치를 멈추고, 어떤 텍스처를 배경으로 남길지 결정하는 것은 코드 생성 문제가 아니라 시각적 의도와 맥락을 통제하는 문제이기 때문이다.

관련 작업:


코드는 캔버스가 될 수 있다. 하지만 캔버스에 무엇을 남길지는 여전히 디렉터가 결정한다.