이 글은 누구를 위한 것인가
- 브라우저에서 고성능 3D 시각화나 GPU 계산이 필요한 팀
- WebGL에서 WebGPU로 전환을 검토하는 개발자
- 데이터 시각화에 GPU 가속을 활용하려는 프론트엔드 개발자
들어가며
WebGPU는 WebGL의 후계자다. 현대 GPU API(Vulkan/Metal/D3D12)를 웹에서 직접 사용해 WebGL 대비 3-5배 빠른 렌더링과 컴퓨트 셰이더를 지원한다. Chrome 113+, Firefox Nightly, Safari TP에서 지원된다.
이 글은 bluefoxdev.kr의 WebGPU 실전 가이드 를 참고하여 작성했습니다.
1. WebGPU vs WebGL 비교
[WebGL vs WebGPU]
WebGL (OpenGL ES 기반):
API: OpenGL 스타일 (상태 기계)
셰이더: GLSL
멀티스레딩: 제한적
컴퓨트: 미지원 (WebGL 2는 부분 지원)
CPU-GPU 동기: 느린 readPixels()
WebGPU (Vulkan/Metal/D3D12 기반):
API: 모던 GPU API (명시적 제어)
셰이더: WGSL (WebGPU Shading Language)
멀티스레딩: Web Worker 지원
컴퓨트: 컴퓨트 셰이더 완전 지원
CPU-GPU 동기: 효율적 버퍼 매핑
[WebGPU 핵심 개념]
GPUDevice: GPU 인터페이스
GPUBuffer: GPU 메모리 버퍼
GPUTexture: 텍스처
GPURenderPipeline: 렌더링 파이프라인 (버텍스 + 프래그먼트)
GPUComputePipeline: 컴퓨트 파이프라인
GPUCommandEncoder: GPU 명령 기록
GPUBindGroup: 리소스 바인딩 그룹
[WGSL 주요 특징]
강타입 (f32, vec3<f32>, mat4x4<f32>)
built-in 변수: @builtin(position), @builtin(vertex_index)
바인딩: @group(0) @binding(0)
셰이더 스테이지: @vertex, @fragment, @compute
2. WebGPU 3D 렌더링 구현
// WebGPU 초기화 및 기본 삼각형 렌더링
class WebGPURenderer {
private device!: GPUDevice;
private context!: GPUCanvasContext;
private format!: GPUTextureFormat;
async initialize(canvas: HTMLCanvasElement) {
// GPU 어댑터 요청
const adapter = await navigator.gpu?.requestAdapter({
powerPreference: 'high-performance',
});
if (!adapter) throw new Error('WebGPU 미지원');
this.device = await adapter.requestDevice({
requiredFeatures: [],
requiredLimits: {
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
},
});
// Canvas 설정
this.context = canvas.getContext('webgpu') as GPUCanvasContext;
this.format = navigator.gpu.getPreferredCanvasFormat();
this.context.configure({
device: this.device,
format: this.format,
alphaMode: 'premultiplied',
});
}
createTriangleRenderer() {
// WGSL 셰이더
const shaderCode = `
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
}
@vertex
fn vsMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
// 화면 좌표 삼각형 (-1 to 1)
var positions = array<vec2<f32>, 3>(
vec2<f32>( 0.0, 0.5),
vec2<f32>(-0.5, -0.5),
vec2<f32>( 0.5, -0.5),
);
var colors = array<vec3<f32>, 3>(
vec3<f32>(1.0, 0.0, 0.0), // 빨강
vec3<f32>(0.0, 1.0, 0.0), // 초록
vec3<f32>(0.0, 0.0, 1.0), // 파랑
);
var output: VertexOutput;
output.position = vec4<f32>(positions[vertexIndex], 0.0, 1.0);
output.color = colors[vertexIndex];
return output;
}
@fragment
fn fsMain(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(input.color, 1.0);
}
`;
const shaderModule = this.device.createShaderModule({ code: shaderCode });
const pipeline = this.device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vsMain',
},
fragment: {
module: shaderModule,
entryPoint: 'fsMain',
targets: [{ format: this.format }],
},
primitive: {
topology: 'triangle-list',
},
});
return pipeline;
}
render(pipeline: GPURenderPipeline) {
const commandEncoder = this.device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: this.context.getCurrentTexture().createView(),
clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}],
});
renderPass.setPipeline(pipeline);
renderPass.draw(3); // 3개 버텍스 (삼각형)
renderPass.end();
this.device.queue.submit([commandEncoder.finish()]);
}
}
// 컴퓨트 셰이더 - GPU 병렬 계산 (N-body 시뮬레이션)
async function runParticleSimulation(device: GPUDevice) {
const PARTICLE_COUNT = 1000;
// 입자 초기 데이터 (x, y, vx, vy)
const particles = new Float32Array(PARTICLE_COUNT * 4);
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles[i * 4 + 0] = Math.random() * 2 - 1; // x
particles[i * 4 + 1] = Math.random() * 2 - 1; // y
particles[i * 4 + 2] = (Math.random() - 0.5) * 0.01; // vx
particles[i * 4 + 3] = (Math.random() - 0.5) * 0.01; // vy
}
// GPU 버퍼 생성
const particleBuffer = device.createBuffer({
size: particles.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
});
device.queue.writeBuffer(particleBuffer, 0, particles);
// 컴퓨트 셰이더
const computeShader = `
struct Particle {
position: vec2<f32>,
velocity: vec2<f32>,
}
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let i = id.x;
if (i >= arrayLength(&particles)) { return; }
var p = particles[i];
// 중력
p.velocity.y -= 0.0001;
// 위치 업데이트
p.position += p.velocity;
// 경계 반사
if (abs(p.position.x) > 1.0) { p.velocity.x *= -1.0; }
if (abs(p.position.y) > 1.0) { p.velocity.y *= -1.0; }
particles[i] = p;
}
`;
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: device.createShaderModule({ code: computeShader }),
entryPoint: 'main',
},
});
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: particleBuffer } }],
});
// 시뮬레이션 실행
function step() {
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(PARTICLE_COUNT / 64));
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(step);
}
step();
}
// Three.js WebGPU 렌더러 (WebGL 코드를 WebGPU로 전환)
// import * as THREE from 'three/webgpu';
// const renderer = new THREE.WebGPURenderer({ canvas });
// await renderer.init();
// 기존 Three.js 코드 그대로 사용 가능
// const scene = new THREE.Scene();
// const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
// const geometry = new THREE.BoxGeometry();
// const material = new THREE.MeshStandardMaterial({ color: 0x6366f1 });
// scene.add(new THREE.Mesh(geometry, material));
마무리
WebGPU는 Three.js/Babylon.js 같은 라이브러리 없이 GPU를 직접 제어할 수 있는 강력한 API다. 컴퓨트 셰이더는 물리 시뮬레이션, 머신러닝, 이미지 처리를 CPU 대비 수십-수백 배 빠르게 실행한다. 당장 Three.js를 버릴 필요는 없다. Three.js r165+의 WebGPURenderer는 기존 Three.js 코드를 WebGPU로 렌더링하는 마이그레이션 경로를 제공한다.