🚀 2026 스타트업 컨퍼런스

웹에서 '고퀄리티 연기' 구현하다 FPS 나락 갔을 때, 쉐이더 한 장으로 성능 잡고 생존하는 법

웹에서 '고퀄리티 연기' 구현하다 FPS 나락 갔을 때, 쉐이더 한 장으로 성능 잡고 생존하는 법

Alex Kim·2026년 1월 5일·3

무거운 라이브러리 없이 Three.js와 쉐이더(Shader) 한 장으로 가볍고 우아한 연기 효과를 만드는 법을 정리합니다. GPU를 활용해 FPS를 방어하세요.

솔직히 말해봅시다. 디자이너가 "이 화면에 분위기 있게 담배 연기 좀 깔아주세요"라고 했을 때, 여러분의 첫 반응은 무엇이었나요?

대부분의 주니어 개발자들은 무거운 파티클 라이브러리를 npm install 하거나, 심지어 고용량 투명 GIF를 배경에 까는 만행을 저지르곤 합니다. 결과는 뻔하죠. 크롬 탭 메모리는 폭발하고, 모바일에서 스크롤을 내릴 때마다 뚝뚝 끊기는 프레임 드랍(Frame Drop)을 경험하게 됩니다. 사용자 경험(UX)을 살리려다 디바이스 배터리를 죽이는 꼴입니다.

브라우저 위에서 고비용의 그래픽 효과를 구현하려면, CPU가 아니라 GPU를 태워야 합니다. 오늘은 무거운 라이브러리 없이, Three.js와 쉐이더(Shader) 한 장으로 가볍고 우아한 연기 효과를 만드는 법을 정리합니다. 복사 붙여넣기만 하지 말고, 원리를 이해해서 본인의 무기로 만드세요.


1. 기본 세팅: 캔버스 준비 (The Setup)

우선 연기가 그려질 도화지가 필요합니다. 복잡한 3D 모델링은 필요 없습니다. 그냥 평평한 판(Plane Geometry) 하나면 충분합니다. 여기에 ShaderMaterial을 입히는 게 시작입니다.

const material = new THREE.ShaderMaterial({
  vertexShader: `
    void main() {
      // 위치 결정: 기본적으로 화면에 딱 붙입니다.
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    void main() {
      // 일단 녹색으로 칠해서 쉐이더가 먹히는지 확인
      gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); 
    }
  `,
  side: THREE.DoubleSide,
  // 디버깅할 땐 와이어프레임 켜보는 습관, 잊지 마세요.
  wireframe: config.material.wireframe 
});

2. 연료 주입: 펄린 노이즈 (Perlin Noise)

연기의 핵심은 '불규칙함'입니다. 하지만 Math.random() 같은 완전한 무작위는 노이즈(TV 지지직거림)처럼 보일 뿐, 자연스럽지 않습니다. 우리가 필요한 건 부드럽게 이어지는 난수, 즉 펄린 노이즈(Perlin Noise) 텍스처입니다.

이 텍스처를 로드해서 프래그먼트 쉐이더(Fragment Shader)로 넘깁니다.

const textureLoader = new THREE.TextureLoader();
// 노이즈 텍스처 로드
const texture = textureLoader.load(config.material.textureURL);

const material = new THREE.ShaderMaterial({
  uniforms: {
    uTexture: new THREE.Uniform(texture), // 쉐이더로 데이터 전송
  },
  // ...
});

💡 Alex's Tip:
쉐이더에서 텍스처를 다룰 땐 UV 좌표 개념이 필수입니다. Vertex Shader에서 varying을 통해 UV 좌표를 Fragment Shader로 넘겨줘야, 각 픽셀이 텍스처의 어느 부분을 참조할지 알 수 있습니다.

3. 매핑: 텍스처 입히기

이제 쉐이더 내부에서 텍스처를 읽어옵니다. texture() 함수를 사용하면 UV 좌표에 맞는 색상값을 가져올 수 있습니다. 노이즈 텍스처가 흑백이라면 RGB 값이 모두 같으니, Red 채널(.r) 하나만 가져다 쓰면 됩니다.

// Fragment Shader
uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
  vec2 textureUv = vUv;
  // 텍스처의 R값(밝기)만 추출
  float textureImpl = texture(uTexture, textureUv).r;
  
  // 흑백 그대로 출력해보기
  gl_FragColor = vec4(textureImpl, textureImpl, textureImpl, 1.0);
}

이 시점에서 화면을 보면, 그냥 회색 구름 같은 이미지가 보일 겁니다. 아직 연기 같지는 않죠.

4. 마스킹: 투명도(Alpha) 조절 (Crucial Step)

여기가 가장 중요합니다. 연기는 '그림'이 아니라 '투명도'로 표현해야 합니다.

  • 흰색 픽셀(값 1.0) → 불투명한 연기
  • 검은색 픽셀(값 0.0) → 투명한 배경
  • 회색 픽셀 → 반투명한 연기 자락

이걸 구현하기 위해 텍스처의 색상 값을 컬러가 아닌 Alpha(투명도) 값으로 사용합니다.

void main() {
  // 색상은 흰색(1,1,1) 고정, 투명도(a)에 텍스처 값 적용
  gl_FragColor = vec4(1.0, 1.0, 1.0, textureImpl);
}

Javascript 코드에서도 transparent: true 옵션을 켜는 걸 깜빡하지 마세요. 이거 안 켜놓고 "쉐이더가 고장 났다"며 밤새는 주니어들을 너무 많이 봤습니다.

5. 생명 불어넣기: 애니메이션 & 리맵핑

정지된 연기는 그냥 얼룩입니다. 연기가 피어오르는 느낌을 주려면 텍스처를 흘려보내야 합니다.

1) 애니메이션 (Flow)
매 프레임마다 uTime 값을 증가시켜 쉐이더로 보냅니다. 텍스처의 Y 좌표에서 시간을 빼주면, 텍스처가 위로 올라가는(좌표는 내려가는) 효과가 납니다.

// Fragment Shader
uniform float uTime;
uniform float uSpeed;

void main() {
  vec2 textureUv = vUv;
  // 시간 흐름에 따라 Y축 이동 (연기가 위로 올라감)
  textureUv.y -= uTime * uSpeed; 
  
  float textureImpl = texture(uTexture, textureUv).r;
  // ...
}

Javascript 쪽에서 texture.wrapT = THREE.RepeatWrapping 설정을 해줘야 텍스처가 끊기지 않고 무한히 반복됩니다.

2) 디테일 보정 (Remapping)
처음 결과물은 연기라기보다 희미한 안개처럼 보일 겁니다. 대비(Contrast)가 부족하기 때문입니다. GLSL의 smoothstep 함수를 사용해 흐리멍덩한 회색 영역을 확실하게 정리해 줍니다.

  • smoothstep(low, high, value): value가 low보다 낮으면 0, high보다 높으면 1, 그 사이는 부드럽게 보간.
// 흐릿한 텍스처 값을 더 선명하게 보정
textureImpl = smoothstep(uRemapLow, uRemapHigh, textureImpl);

이 한 줄이 들어가야 비로소 뭉게구름이 아닌, 밀도 있는 '진짜 연기'의 질감이 나옵니다.


마치며: 복붙보다 중요한 것

여기까지 따라왔다면, CPU 점유율은 거의 쓰지 않으면서 GPU 가속만으로 매끄럽게 피어오르는 연기 효과를 얻었을 겁니다.

이 글에서 가져가야 할 것은 단순한 코드가 아닙니다.

  1. 이미지 대신 수학(노이즈)을 사용하는 효율성
  2. 색상 값을 투명도나 좌표로 치환하는 유연한 사고
  3. smoothstep 같은 내장 함수를 활용한 디테일 보정

이 세 가지가 바로 그래픽스 엔지니어링의 기초 체력입니다.

화려한 라이브러리 뒤에 숨지 마세요. 쉐이더라는 날카로운 도구를 손에 쥐면, 100줄짜리 라이브러리 코드를 단 10줄의 GLSL로 대체하고 FPS와 퇴근 시간을 동시에 지킬 수 있습니다.

오늘 밤, 여러분의 코드가 조금 더 가벼워지길 바랍니다.

Alex Kim
Alex KimAI 인프라 리드

모델의 정확도보다 추론 비용 절감을 위해 밤새 CUDA 커널을 깎는 엔지니어. 'AI는 마법이 아니라 전기세와 하드웨어의 싸움'이라고 믿습니다. 화려한 데모 영상 뒤에 숨겨진 병목 현상을 찾아내 박살 낼 때 가장 큰 희열을 느낍니다.

Alex Kim님의 다른 글

댓글 0

첫 번째 댓글을 남겨보세요!