WebGPU + Three.js TSL로 브라우저에서 10만 개 파티클을 2ms로 — 컴퓨트 셰이더와 GPGPU가 만든 새로운 성능 기준
WebGL로 파티클 시스템을 만들다가 수만 개를 넘기는 순간 프레임이 뚝뚝 끊기는 경험, 저도 분명히 겪었습니다. CPU에서 매 프레임 위치를 계산하고 버퍼에 복사하고 GPU에 전달하는 이 구조적 병목 앞에서, 한동안 "WebGL에서는 이게 한계구나"라고 받아들였습니다. 그런데 2026년 지금, 그 공식이 바뀌었습니다. WebGPU는 컴퓨트 셰이더를 통해 CPU 개입 없이 GPU 안에서 물리 연산과 렌더링을 모두 처리하게 해주고, 중급 이상 GPU 기준으로 10만 개 파티클을 2ms 미만으로 구동하는 게 브라우저에서 가능해졌습니다.
2025년 9월 Safari 26 출시로 Chrome, Firefox, Edge, Safari 전 주요 브라우저가 WebGPU를 기본 지원하게 되었고, 전 세계 브라우저 커버리지가 약 70%에 도달했습니다. 아직 "실험적 기술"이라는 인식이 남아 있을 수 있지만, 지금은 프로덕션 적용을 진지하게 검토할 시점입니다. 특히 WebGL이나 Canvas API를 써본 프론트엔드 개발자라면 Three.js TSL이라는 브리지 덕분에 WGSL을 처음부터 익히지 않아도 진입이 가능합니다.
핵심 개념
WebGPU가 WebGL과 근본적으로 다른 이유
WebGL은 OpenGL ES 2.0을 웹용으로 래핑한 API입니다. 2011년 기준으로는 혁신적이었지만 2020년대 GPU 아키텍처와는 거리가 있고, CPU 드라이버가 GPU 명령을 실시간으로 검증하고 번역하는 구조라 오버헤드가 큽니다. 그리고 무엇보다, 렌더링 파이프라인 밖에서 GPU 병렬 연산을 돌리는 컴퓨트 셰이더가 없습니다.
WebGPU는 Direct3D 12, Metal, Vulkan의 설계 철학을 웹으로 가져왔습니다.
| 구분 | WebGL | WebGPU |
|---|---|---|
| 기반 | OpenGL ES 2.0 | D3D12 / Metal / Vulkan |
| 컴퓨트 셰이더 | 없음 | 지원 |
| CPU 오버헤드 | 높음 (드라이버 검증) | 낮음 (명시적 제어) |
| 멀티스레드 렌더링 | 불가 | 지원 |
| 셰이더 언어 | GLSL | WGSL |
| 메모리 제어 | 암묵적 | 명시적 |
컴퓨트 셰이더 — 이게 핵심입니다
솔직히 말씀드리면, WebGPU의 나머지 장점들은 "WebGL보다 낫다"는 정도지만 컴퓨트 셰이더는 차원이 다릅니다. 렌더링 파이프라인과 완전히 독립적으로 GPU 코어에서 병렬 연산을 수행할 수 있거든요.
파티클 시스템을 예로 들면, 기존 WebGL 방식에서는 CPU가 매 프레임 각 파티클 위치를 갱신하고 GPU 메모리(VRAM)에 업로드하는 작업을 반복합니다. 10만 개면 CPU가 감당을 못하는 수준입니다. 컴퓨트 셰이더를 쓰면 위치 갱신 자체를 GPU가 처리합니다. CPU는 "셰이더 실행해"라는 명령만 보내면 됩니다.
// WGSL 컴퓨트 셰이더 — 파티클 위치 업데이트
@group(0) @binding(0) var<storage, read_write> positions: array<vec4f>;
@group(0) @binding(1) var<storage, read_write> velocities: array<vec4f>;
@group(0) @binding(2) var<uniform> params: SimParams;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3u) {
let i = id.x;
if (i >= arrayLength(&positions)) { return; }
var pos = positions[i];
var vel = velocities[i];
// 중력 적용
vel.y -= params.gravity * params.deltaTime;
// 위치 갱신
pos += vel * params.deltaTime;
positions[i] = pos;
velocities[i] = vel;
}workgroup_size(256): GPU는 스레드를 workgroup 단위로 묶어 실행합니다. 256개 파티클이 GPU 코어에서 동시에 처리되는 구조로, CPU의 for 루프와 본질적으로 다릅니다.
TSL — WGSL을 직접 쓰지 않아도 됩니다
WGSL이 낯설어서 진입 장벽을 느끼신다면, Three.js의 TSL(Three Shader Language)로 먼저 접근하는 방법도 있습니다. JavaScript 문법으로 GPU 코드를 작성하면 TSL이 WGSL/GLSL로 컴파일해주는 방식인데, 저도 처음에는 "이게 진짜 GPU에서 도는 게 맞나?" 싶었습니다. 맞습니다.
import {
Fn, float, instanceIndex, deltaTime, If
} from 'three/tsl';
// JavaScript처럼 보이지만 GPU에서 실행됩니다
const updateParticles = Fn(() => {
const i = instanceIndex;
const pos = positionBuffer.element(i);
const vel = velocityBuffer.element(i);
vel.y.addAssign(gravityUniform.mul(deltaTime).negate());
pos.addAssign(vel.mul(deltaTime));
positionBuffer.element(i).assign(pos);
velocityBuffer.element(i).assign(vel);
})().compute(PARTICLE_COUNT);TSL(Three Shader Language): Three.js r171+에서 지원하는 JavaScript 기반 셰이더 DSL입니다. WGSL 학습 없이도 WebGPU 컴퓨트 파이프라인을 활용할 수 있게 해주는 추상화 레이어입니다.
Three.js WebGPURenderer — 렌더러 교체 한 줄
Three.js r171에서 제가 가장 먼저 해본 게 렌더러 교체였는데, 진짜 한 줄이었습니다. WebGL 2 자동 폴백도 기본으로 제공되니 호환성 걱정도 많이 줄었습니다.
// 기존 WebGL
// import { WebGLRenderer } from 'three';
// const renderer = new WebGLRenderer({ antialias: true });
// WebGPU로 전환 — 이것만 바꾸면 됩니다
import WebGPURenderer from 'three/addons/renderers/common/WebGPURenderer.js';
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init(); // WebGPU는 비동기 초기화가 필요합니다
document.body.appendChild(renderer.domElement);실전 적용
예시 1: GPGPU 파티클 시스템 (Three.js + TSL)
**GPGPU(General Purpose GPU Computing, 범용 GPU 연산)**는 물리나 시뮬레이션처럼 렌더링과 무관한 연산도 GPU에 맡기는 방식입니다. 10만 개 파티클 예시인데, React Three Fiber + TSL 조합으로 현업에서 바로 활용할 수 있는 구조입니다.
import * as THREE from 'three';
import WebGPURenderer from 'three/addons/renderers/common/WebGPURenderer.js';
// ※ StorageBufferAttribute의 import 경로는 Three.js 버전에 따라 달라질 수 있습니다
import StorageBufferAttribute from 'three/addons/renderers/common/StorageBufferAttribute.js';
import { Fn, instanceIndex, deltaTime, storage, float, If } from 'three/tsl';
const PARTICLE_COUNT = 100_000;
async function initGPGPU() {
const renderer = new WebGPURenderer();
await renderer.init();
// scene, camera 초기화는 생략 — 일반적인 Three.js 씬 구성과 동일합니다
// GPU 스토리지 버퍼 — CPU 메모리가 아닌 VRAM에 상주합니다
const positions = new StorageBufferAttribute(
new Float32Array(PARTICLE_COUNT * 4), 4
);
const velocities = new StorageBufferAttribute(
new Float32Array(PARTICLE_COUNT * 4), 4
);
// 초기 데이터 설정
for (let i = 0; i < PARTICLE_COUNT; i++) {
positions.setXYZW(i,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
1
);
velocities.setXYZW(i,
(Math.random() - 0.5) * 0.1,
Math.random() * 0.2,
(Math.random() - 0.5) * 0.1,
0
);
}
const posStorage = storage(positions, 'vec4', PARTICLE_COUNT);
const velStorage = storage(velocities, 'vec4', PARTICLE_COUNT);
const computeUpdate = Fn(() => {
const i = instanceIndex;
const pos = posStorage.element(i);
const vel = velStorage.element(i);
// 중력 + 공기 저항
vel.y.addAssign(float(-9.8).mul(deltaTime));
vel.mulAssign(float(0.999));
pos.addAssign(vel.mul(deltaTime));
// 바닥 충돌
If(pos.y.lessThan(-5), () => {
pos.y.assign(-5);
vel.y.assign(vel.y.negate().mul(0.6));
});
posStorage.element(i).assign(pos);
velStorage.element(i).assign(vel);
})().compute(PARTICLE_COUNT);
// 컴퓨트 결과를 복사 없이 렌더 파이프라인에 연결
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', positions);
const material = new THREE.PointsMaterial({ size: 0.02, color: 0x88ccff });
const particles = new THREE.Points(geometry, material);
function animate() {
requestAnimationFrame(animate);
renderer.compute(computeUpdate); // GPU에서 위치 갱신
renderer.render(scene, camera); // 갱신된 버퍼를 바로 렌더링
}
animate();
}| 코드 포인트 | 설명 |
|---|---|
StorageBufferAttribute |
VRAM에 직접 할당되는 GPU 버퍼. CPU↔GPU 복사가 없음 |
renderer.compute() |
컴퓨트 셰이더 실행. 렌더링과 독립적으로 동작 |
geometry.setAttribute('position', positions) |
컴퓨트 결과를 복사 없이 렌더 파이프라인에 연결 |
deltaTime |
TSL 내장 변수. 프레임 독립적 물리 계산에 필수 |
컴퓨트 셰이더가 WebGPU의 전부는 아닙니다. 셰이더 자체도 달라졌다는 게 중요한 변화인데, TSL로 작성하는 디졸브 애니메이션을 보면 그 차이가 느껴집니다.
예시 2: 노이즈 기반 텍스트 디졸브 애니메이션 (TSL)
요즘 포트폴리오나 랜딩 페이지에서 자주 보이는, 텍스트가 파티클로 흩어지는 효과입니다. MSDF 폰트(Multi-channel Signed Distance Field — 확대해도 선명함을 유지하는 벡터 폰트 렌더링 기법)와 TSL 노이즈 셰이더를 결합하면 구현할 수 있습니다.
import { MeshBasicNodeMaterial } from 'three/webgpu';
import {
Fn, uniform, uv, time, vec4, vec3,
mx_noise_float, smoothstep, mix
} from 'three/tsl';
const createDissolveShader = () => {
const dissolveProgress = uniform(0); // 0 = 원본, 1 = 완전 디졸브
const fragmentShader = Fn(() => {
// uv는 TSL 내장 노드 — 파라미터로 받지 않아도 됩니다
const noiseScale = uniform(5.0);
const noiseValue = mx_noise_float(
vec3(uv().mul(noiseScale), time.mul(0.5))
);
// 노이즈 임계값으로 픽셀 제거
const threshold = dissolveProgress.add(noiseValue.mul(0.3));
const alpha = smoothstep(threshold, threshold.add(0.1), noiseValue);
// 경계 글로우 효과 — 타오르는 느낌
const edgeGlow = smoothstep(
threshold.sub(0.05), threshold, noiseValue
).mul(vec4(0.3, 0.8, 1.0, 1.0)); // 시안 색상 글로우
return mix(vec4(1, 1, 1, alpha), edgeGlow, edgeGlow.w);
});
const material = new MeshBasicNodeMaterial();
material.fragmentNode = fragmentShader;
material.transparent = true;
return { material, dissolveProgress };
};
// GSAP으로 디졸브 진행도 애니메이션
const { material, dissolveProgress } = createDissolveShader();
gsap.to(dissolveProgress, {
value: 1,
duration: 2,
ease: 'power2.inOut'
});uniform: CPU에서 GPU 셰이더로 값을 전달하는 변수입니다. TSL의
uniform()으로 생성하면 JavaScript에서.value를 바꾸는 것만으로 셰이더 파라미터가 실시간 갱신됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 극적인 성능 향상 | byteiota 벤치마크 기준, 동일 씬에서 WebGL 대비 드로우 가능 오브젝트 수 약 10배 증가 (15,000개 15FPS → 200,000개 60FPS). 씬 구성과 하드웨어에 따라 결과는 달라집니다 |
| 컴퓨트 셰이더 | 물리·AI 연산이 메인 스레드에서 완전히 분리되어 UI 반응성 유지 |
| Zero-copy 렌더링 | VRAM의 컴퓨트 결과를 CPU 복사 없이 바로 렌더링에 활용 |
| 멀티스레드 커맨드 레코딩 | 복잡한 씬의 드로우 콜 오버헤드를 멀티코어로 분산 |
| W3C 표준 | 장기적 API 안정성 보장. 브라우저 벤더 의존도 낮음 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 브라우저 호환성 | 2026년 기준 전 세계 커버리지 약 70% | WebGL 2 자동 폴백 활용 (Three.js 기본 제공) |
| 학습 곡선 | GPU 파이프라인, 버퍼 레이아웃, 셰이더 개념 필요 | TSL로 진입 후 점진적으로 WGSL 학습 |
| 단순 씬에서 이점 없음 | 드로우 콜이 적거나 셰이더가 단순한 경우 | 파티클·시뮬레이션 등 병렬성이 높은 작업에만 적용 |
| 디버깅 도구 미성숙 | Chrome DevTools 외 전용 디버거 생태계가 작음 | webgpureport.org로 기기별 기능 확인, 단계별 로깅 활용 |
| 모바일 지원 | iOS는 Safari 26+, Android는 Chrome 121+(Android 12+) | 폴백 전략 필수. 모바일 타겟이라면 신중히 검토 |
WGSL(WebGPU Shading Language): WebGPU의 공식 셰이더 언어입니다. Rust 문법에서 영감을 받아 타입 안전성이 강하고, GLSL과는 문법이 상당히 다릅니다. Three.js TSL을 쓰면 직접 작성하지 않아도 됩니다.
실무에서 가장 흔한 실수
-
WebGPU 초기화를 동기 코드로 작성하는 경우 —
renderer.init()는 Promise를 반환합니다.await없이 씬을 구성하면 렌더러가 준비되기 전에 드로우 콜이 발생해 조용히 실패합니다. 이 실수를 한 번 겪고 나면async/await로 초기화 순서를 보장하는 게 얼마나 중요한지 바로 깨닫게 됩니다. -
매 프레임 CPU에서 GPU 버퍼를 업로드하는 구조 유지 — WebGL 방식 그대로
geometry.attributes.position.needsUpdate = true를 반복 호출하면 WebGPU의 이점이 사라집니다. 애니메이션 데이터는 처음부터StorageBufferAttribute로 VRAM에 두고, 컴퓨트 셰이더로만 갱신하는 구조로 설계하는 것이 핵심입니다. -
단일 workgroup에 너무 많은 스레드를 지정하는 경우 —
@compute @workgroup_size(1024)처럼 workgroup 크기를 무작정 키우면 일부 GPU에서 한계를 초과합니다. 대부분의 GPU는 workgroup당 256~512 스레드를 권장하며,device.limits.maxComputeInvocationsPerWorkgroup으로 기기별 한계를 확인해 볼 수 있습니다.
마치며
10만 개 파티클이 지금의 현실적인 출발점이고, workgroup 튜닝과 인스턴싱을 더하면 100만 개도 충분히 도달 가능한 목표입니다. Three.js r171 + TSL 조합은 그 진입 장벽을 크게 낮춰주고 있고, 저는 이미 일부 프로덕션 프로젝트에 적용하고 있습니다.
당장 대규모 파티클이 필요한 프로젝트가 없더라도, 지금 작게 시작해두면 나중에 성능 문제가 생겼을 때 훨씬 빠르게 대응할 수 있습니다.
-
브라우저 호환 여부 먼저 확인 — webgpureport.org에 접속하면 현재 기기의 WebGPU 지원 기능과 한계를 한눈에 볼 수 있습니다. Chrome 113+ 또는 Safari 26+ 환경이라면 바로 시작 가능합니다.
-
Three.js r171 기반 빈 WebGPU 씬 세팅 —
pnpm add three로 설치 후WebGPURenderer로 씬을 띄워보는 것부터 시작해 볼 수 있습니다. 기존 Three.js 경험이 있다면 렌더러 교체만으로 첫 WebGPU 경험을 할 수 있습니다. -
Wawa Sensei의 GPGPU 파티클 강좌 수강 — React Three Fiber + TSL + WebGPU GPGPU를 단계적으로 배울 수 있는 무료 강좌입니다. 컴퓨트 셰이더를 처음 접한다면 이 순서가 가장 자연스럽습니다.
참고 자료
- WebGPU API | MDN Web Docs
- WebGPU: The Complete Guide to Modern Graphics and Compute on the Web (2026) | explainx.ai
- WebGPU 2026: 70% Browser Support, 15x Performance Gains | byteiota
- WebGPU Hits Critical Mass: All Major Browsers Now Ship It | webgpu.com
- WebGL vs WebGPU: The Performance Gap | Medium
- What's New in Three.js (2026): WebGPU, New Workflows & Beyond | utsubo.com
- Unlock GPU computing with WebGPU — WWDC25 | Apple Developer
- Particles, Progress, and Perseverance: A Journey into WebGPU Fluids | Codrops
- Field Guide to TSL and WebGPU | Maxime Heckel
- Interactive Galaxy with WebGPU Compute Shaders | Three.js Roadmap
- WebGPU Unleashed: A Practical Tutorial
- WebGPU Support | Babylon.js Documentation
- GPGPU particles with TSL & WebGPU | Wawa Sensei
- WebGPU | Can I use
- Awesome WebGPU | GitHub