WebGL 프로젝트에서 가장 자주 마주치는 문제는 "느리다"입니다. 화면이 버벅이고 프레임이 떨어지는 원인, 대부분은 DrawCall에 있습니다. LG ThinQ WebGL 엔진(MAU 150만 규모)을 TV·모바일·PC에 배포하며 쌓은 최적화 경험을 공유합니다.
DrawCall이란?
DrawCall은 CPU가 GPU에게 "이걸 그려라"라고 명령하는 호출입니다. 화면에 보이는 각 오브젝트마다 최소 1회의 DrawCall이 발생하고, 재질(Material)이 다르면 추가됩니다.
문제는 DrawCall 자체의 비용입니다. GPU는 빠르지만, CPU→GPU 간 상태 전환(State Change)에 오버헤드가 큽니다. DrawCall이 100개일 때와 1,000개일 때의 차이는 렌더링 복잡도보다 이 전환 비용에서 옵니다.
DrawCall 100개 → 60 FPS (쾌적) DrawCall 500개 → 30 FPS (체감 시작) DrawCall 1000개+ → 15 FPS 이하 (사용 불가) * 디바이스, 씬 복잡도에 따라 다름
최적화 전략 5가지
1. 지오메트리 병합 (Geometry Merging)
같은 Material을 사용하는 오브젝트들은 하나로 합칠 수 있습니다.
// Before: 오브젝트 100개 = DrawCall 100개
for (let i = 0; i < 100; i++) {
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
}
// After: 병합 = DrawCall 1개
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
scene.add(mergedMesh);
언제 쓰나: 정적 오브젝트(배경, 건물, 가구 등)에 효과적입니다. 개별 오브젝트를 클릭하거나 움직여야 하는 경우에는 적합하지 않습니다.
2. 인스턴싱 (Instanced Rendering)
동일한 지오메트리를 위치/크기/회전만 다르게 여러 번 그려야 할 때, InstancedMesh를 사용하면 1회의 DrawCall로 수천 개를 그릴 수 있습니다.
const mesh = new THREE.InstancedMesh(geometry, material, 1000);
for (let i = 0; i < 1000; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(Math.random() * 100, 0, Math.random() * 100);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh); // DrawCall 1개로 1000개 렌더링
언제 쓰나: 나무, 파티클, 반복되는 건축 모듈 등 동일 형태가 대량으로 필요한 경우.
3. 텍스처 아틀라스 (Texture Atlas)
Material이 다른 이유가 텍스처만 다른 경우, 여러 텍스처를 하나의 큰 이미지에 합치면 Material을 통합할 수 있습니다.
Before: 나무 텍스처 + 풀 텍스처 + 돌 텍스처 = Material 3개 = DrawCall 3개 After: 아틀라스 1장 + UV 좌표 조정 = Material 1개 = DrawCall 1개
게임 업계에서 오래 사용해온 기법이며, WebGL에서도 동일하게 적용됩니다.
4. LOD (Level of Detail)
카메라에서 멀리 있는 오브젝트에 고해상도 모델을 사용할 필요가 없습니다. 거리에 따라 폴리곤 수가 다른 모델을 교체합니다.
const lod = new THREE.LOD(); lod.addLevel(highDetailMesh, 0); // 가까이: 10,000 폴리곤 lod.addLevel(medDetailMesh, 50); // 중간: 1,000 폴리곤 lod.addLevel(lowDetailMesh, 200); // 멀리: 100 폴리곤 scene.add(lod);
BIM 시뮬레이터처럼 수십만 폴리곤의 대규모 모델을 다룰 때 필수적인 기법입니다.
5. 프러스텀 컬링 & 오클루전 컬링
프러스텀 컬링: 카메라 시야에 들어오지 않는 오브젝트는 렌더링에서 제외합니다. Three.js에서 기본 지원하지만, 대규모 씬에서는 공간 분할(옥트리, BVH) 구조를 추가로 적용해야 효과가 극대화됩니다.
오클루전 컬링: 다른 오브젝트에 완전히 가려진 오브젝트를 제외합니다. 구현 난이도가 높지만, 건축/실내 시뮬레이션에서 큰 효과를 발휘합니다.
메모리 관리
DrawCall과 함께 메모리 관리도 중요합니다. 특히 모바일이나 TV 환경에서는 가용 메모리가 제한적입니다.
텍스처 압축
PNG/JPG 대신 GPU 압축 포맷(Basis Universal, KTX2)을 사용하면 GPU 메모리 사용량을 1/4~1/8로 줄일 수 있습니다.
리소스 해제
씬 전환이나 오브젝트 제거 시 geometry.dispose(), material.dispose(), texture.dispose()를 명시적으로 호출해야 합니다. 가비지 컬렉터가 GPU 리소스를 자동으로 해제하지 않습니다.
메모리 버짓
디바이스별 메모리 예산을 사전에 설계합니다. 예를 들어 TV 환경이라면 텍스처 총량을 256MB 이내로 제한하고, 초과 시 저해상도 텍스처로 폴백합니다.
프로파일링 도구
최적화의 시작은 측정입니다.
- Chrome DevTools → Performance: 프레임 타임, CPU/GPU 사용률 확인
- Spector.js: WebGL DrawCall을 하나하나 캡처해서 분석
- Three.js renderer.info:
renderer.info.render.calls로 현재 DrawCall 수 확인 - stats.js: 실시간 FPS 모니터링
console.log(renderer.info.render.calls); // DrawCall 수 console.log(renderer.info.render.triangles); // 삼각형 수 console.log(renderer.info.memory.textures); // 텍스처 수 console.log(renderer.info.memory.geometries); // 지오메트리 수
정리
WebGL 성능 최적화는 결국 "GPU에게 불필요한 일을 시키지 않는 것"입니다.
- DrawCall을 줄여라 (병합, 인스턴싱, 아틀라스)
- 안 보이는 건 그리지 마라 (LOD, 컬링)
- 메모리를 의식적으로 관리하라 (압축, 해제, 버짓)
- 반드시 측정하고 나서 최적화하라 (프로파일링)
다음 글에서는 가장 극한의 최적화가 필요한 환경, TV(webOS)에서의 WebGL 최적화 경험을 다루겠습니다.
WebGL, 성능최적화, DrawCall, Three.js, GPU