Three.js 렌더링 성능 최적화와 병목 현상 해결: Stats와 GUI 활용 사례

문제 소개

Three.js를 활용해 3D 애니메이션을 개발하면서 프레임 드랍이 발생하는 성능 이슈를 겪었습니다.

프로젝트 초반에는 간단한 애니메이션 구현으로 큰 문제가 없었지만, 점차 복잡한 씬 구성과 다수의 객체 렌더링으로 인해 렌더링 성능 저하와 메모리 사용량 증가가 나타났습니다.

특히 병목 현상을 구체적으로 파악하고 개선하기 위한 실질적인 데이터와 도구의 필요성을 느꼈습니다.

직면한 문제

  • 프레임 드랍(FPS 감소): 복잡한 애니메이션과 다수 객체 렌더링으로 인해 FPS가 일정 이하로 떨어져 사용자 경험에 악영향을 미쳤습니다.

  • 병목 현상 파악 어려움: 코드가 복잡해짐에 따라 성능 문제의 원인을 특정하기 어려웠습니다.

  • 리소스 낭비: 지속적인 requestAnimationFrame 호출로 필요하지 않은 상황에서도 GPU와 CPU 리소스가 낭비되었습니다.

해결책

Stats.js를 활용한 성능 모니터링

  • 렌더링 시간(ms), FPS, 메모리 사용량(MB)을 실시간으로 확인하며 병목 지점을 분석했습니다.
  • 특정 코드 구간(begin, end)을 측정해 성능 문제를 초점적으로 확인했습니다.

Lil GUI로 동적 설정 추가

  • Lil GUI를 사용해 객체의 속성값과 카메라 설정을 동적으로 조정했습니다.
  • 실시간으로 속성값을 변경하며 최적의 성능 설정을 탐색했습니다.

On-Demand 렌더링

  • 사용자의 입력(OrbitControls) 또는 특정 이벤트(window resize)에만 렌더링이 발생하도록 개선했습니다.

  • 병목 현상 해결

  • 반복적인 애니메이션 코드 최적화

  • 복잡한 연산을 줄이고 GPU 연산으로 대체

  • 코드와 성능 테스트

병목 현상 해결

  1. 반복적인 애니메이션 코드 최적화
  • 불필요한 연산을 줄이고 GPU 연산으로 대체했습니다.
  1. 불필요한 렌더링 호출제거
  • requestAnimationFrame 호출을 줄이고 이벤트 기반 렌더링을 적용했습니다.

초기 코드 (문제 발생)


import Stats from 'three/examples/jsm/libs/stats.module';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const stats = new Stats();
document.body.appendChild(stats.dom);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

const controls = new OrbitControls(camera, renderer.domElement);

function animate() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  stats.begin();
  renderer.render(scene, camera);
  stats.end();
}

animate();

문제점 분석

  • FPS: 60 → 25로 감소 (초당 렌더링 프레임 감소)
  • 메모리 사용량: 80MB → 150MB 증가
  • 병목 원인: 불필요한 requestAnimationFrame 호출과 지속적인 렌더링

개선된 코드 (On-Demand 렌더링 적용)

import Stats from 'three/examples/jsm/libs/stats.module';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import GUI from 'lil.gui';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const stats = new Stats();
document.body.appendChild(stats.dom);

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshNormalMaterial();
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', render); // OrbitControls 변화 시 렌더링

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  render();
});

const gui = new GUI();
const cubeFolder = gui.addFolder('Cube Rotation');
cubeFolder.add(cube.rotation, 'x', 0, Math.PI * 2).name('Rotation X');
cubeFolder.add(cube.rotation, 'y', 0, Math.PI * 2).name('Rotation Y');
cubeFolder.open();

function render() {
  renderer.render(scene, camera);
}

render();

성능 비교 결과

성능 비교 결과

결론 및 실제 적용 사례

Stats.js와 Lil GUI를 통해 병목 현상을 정확히 파악하고, On-Demand 렌더링으로 성능 최적화를 이뤄냈습니다.

이 개선 사항은 복잡한 3D 씬 구성에서도 안정적인 성능을 보장하며, 실제로 기업 내 대규모 3D 웹 프로젝트에 적용해 리소스 사용량을 40% 절감하는 성과를 거두었습니다.

출처: Three.js Panel

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글