본문 바로가기
Graphic/ThreeJS

09 Three.js 디버그 UI - lil-gui 사용법

by curious week 2025. 7. 23.

Three.js 디버그 UI - lil-gui 사용법

1. 디버그 UI의 중요성

창의적인 프로젝트에서 핵심은 빠르고 유연하게 수정할 수 있어야 한다는 점입니다. 디자이너나 클라이언트가 실시간으로 색상, 위치, 속도 등을 조절할 수 있어야 더 나은 결과를 얻을 수 있습니다.

2. lil-gui 소개 (vs dat.GUI)

디버그 UI 종류: dat.GUI / lil-gui / control-panel / ControlKit / Uil / Tweakpane / Guify / Oui

기존의 dat.GUI는 유지보수 부족으로 lil-gui가 대체하게 되었습니다. lil-gui는 작고 가볍고, 사용성이 좋으며 업데이트도 잘 이루어지고 있습니다.

3. 설치 및 초기 설정

npm install lil-gui
import GUI from 'lil-gui';

const gui = new GUI();

4. 기본 사용법

  • Range 범위 - 최소값과 최대값을 갖는 숫자 
  • Color 색상 - 다양한 형식의 색상에 대해 
  • Text 텍스트 - 간단한 텍스트의 경우 
  • Checkbox 체크박스 —부울( true또는 false) 용 
  • Select 선택 - 값 목록에서 선택 
  • Button 버튼 - 기능을 트리거합니다

숫자 조절

gui.add(mesh.position, 'y', -3, 3, 0.01);
  • .min(), .max(), .step() 으로 체이닝 가능
  • .name()으로 라벨 설정

불리언 (체크박스)

gui.add(mesh, 'visible');

색상

const debugObject = {
  color: '#ff0000',
};
gui.addColor(debugObject, 'color').onChange(() => {
  material.color.set(debugObject.color);
});

함수 호출 버튼

const debugObject = {
  spin: () => {
    gsap.to(mesh.rotation, { y: mesh.rotation.y + Math.PI * 2, duration: 1 });
  },
};
gui.add(debugObject, 'spin');

5. 동적 geometry 재생성

BoxGeometry의 subdivision 값을 동적으로 조정하려면 다음과 같이 처리합니다.

debugObject.subdivision = 2;

gui.add(debugObject, 'subdivision', 1, 20, 1).onFinishChange(() => {
  mesh.geometry.dispose();
  mesh.geometry = new THREE.BoxGeometry(
    1,
    1,
    1,
    debugObject.subdivision,
    debugObject.subdivision,
    debugObject.subdivision,
  );
});

6. 폴더로 정리

const cubeFolder = gui.addFolder('Cube Controls');
cubeFolder.add(mesh, 'visible');
cubeFolder.add(material, 'wireframe');

7. GUI 설정 옵션

const gui = new GUI({
  width: 300,
  title: '디버그 UI',
  closeFolders: false,
});

닫기/숨기기/보이기

gui.hide();

window.addEventListener('keydown', (event) => {
  if (event.key === 'h') {
    gui.show(gui._hidden);
  }
});

8. 팁

  • debugObject를 잘 활용하면 색상, 위치, 기능 등을 깔끔하게 관리할 수 있습니다.
  • geometry 재생성 시 .dispose()를 반드시 호출해 메모리 누수 방지
  • 프로젝트 시작부터 디버그 UI를 설정해두면 유지 관리가 쉬워집니다.

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import gsap from 'gsap';
import GUI from 'lil-gui';

/**
 * Debug
 */
// Object Property만 수정 가능
const gui = new GUI();

/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl');

// Scene
const scene = new THREE.Scene();

/**
 * Object
 */
const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2);
const material = new THREE.MeshBasicMaterial({ color: '#5691a3' });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// gui.add(mesh.position, 'y').min(-3).max(3).step(0.01).name('elevation y');
gui.add(mesh.position, 'y', -3, 3, 0.01).name('elevation y'); // y축 위치 조절 (위와 동일)

gui.add(mesh, 'visible'); // 끄고 켜기

// gui.add(material, 'wireframe');
gui.add(mesh.material, 'wireframe'); // 와이어 프레임 켜기 (위와 동일)

// gui.addColor(material, 'color'); // 색상 변경
gui.addColor(material, 'color').onChange((value) => {
  // 생상 값 얻기
  console.log('value has changed :', material.color.getHexString());
  console.log('material.color === ', value.getHexString());
});

/*
동작하지 않음.
let myVariable = 1337;
gui.add(myVariable, 'myVariable');
아래처럼 같은 이름을 가진 객체 형태만 사용할 수 있다. ( 현재 기능은 없음. )
*/
const myObject = {
  myVariable: 1337,
};
gui.add(myObject, 'myVariable'); // 기능 없음.

/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

window.addEventListener('resize', () => {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  100,
);
camera.position.x = 1;
camera.position.y = 1;
camera.position.z = 2;
scene.add(camera);

// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

/**
 * Animate
 */
const clock = new THREE.Clock();

const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import gsap from 'gsap';
import GUI from 'lil-gui';

/**
 * Debug
 */
const gui = new GUI({
  width: 300, // gui 창 넓이
  title: 'Nice debug UI', // 이름 지정
  closeFolders: false, // 최초 폴더 열림 여부
});
// gui.close();
gui.hide();

// h 키로 gui 보기/숨기기
window.addEventListener('keydown', (event) => {
  if (event.key == 'h') {
    gui.show(gui._hidden);
  }
});
const debugObject = {};

/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl');

// Scene
const scene = new THREE.Scene();

/**
 * Object
 */
debugObject.color = '#5691a3';

const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2);
const material = new THREE.MeshBasicMaterial({
  color: debugObject.color,
  wireframe: true,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const cubTweaks = gui.addFolder('Awesome cube'); // 하나의 폴더에 담기
cubTweaks.close(); // 호출 시 기본값 폴더 닫음.

cubTweaks.add(mesh.position, 'y').min(-3).max(3).step(0.01).name('elevation y');

cubTweaks.add(mesh, 'visible'); // 끄고 켜기

cubTweaks.add(mesh.material, 'wireframe'); // 와이어 프레임 켜기 (위와 동일)

cubTweaks.addColor(debugObject, 'color').onChange(() => {
  material.color.set(debugObject.color);
});

// 객체 회전 시키기
debugObject.spin = () => {
  gsap.to(mesh.rotation, { y: mesh.rotation.y - Math.PI * 2 });
};
cubTweaks.add(debugObject, 'spin');

debugObject.subdivision = 2;
cubTweaks
  .add(debugObject, 'subdivision')
  .min(1)
  .max(20)
  .step(1)
  .onFinishChange(() => {
    // onChange는 값이 너무 많이 변해서 GPU memory 성능저하를 일으킬 수 있음.
    mesh.geometry.dispose(); // 이전 값과 대치
    mesh.geometry = new THREE.BoxGeometry(
      1,
      1,
      1,
      debugObject.subdivision,
      debugObject.subdivision,
      debugObject.subdivision,
    );
  });

/**
 * Sizes
 */
const sizes = {
  width: window.innerWidth,
  height: window.innerHeight,
};

window.addEventListener('resize', () => {
  // Update sizes
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;

  // Update camera
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();

  // Update renderer
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(
  75,
  sizes.width / sizes.height,
  0.1,
  100,
);
camera.position.x = 1;
camera.position.y = 1;
camera.position.z = 2;
scene.add(camera);

// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

/**
 * Animate
 */
const clock = new THREE.Clock();

const tick = () => {
  const elapsedTime = clock.getElapsedTime();

  // Update controls
  controls.update();

  // Render
  renderer.render(scene, camera);

  // Call tick again on the next frame
  window.requestAnimationFrame(tick);
};

tick();