BabylonJS 21회차 : 성능 측정 방법(프레임/드로우콜)
목표: 무엇이 느린지 “측정”할 수 있습니다.
핵심 개념: Inspector, Instrumentation, FPS
실습: 오브젝트 수를 증가시키며 병목 찾기
산출물: “성능 측정 스냅샷”
요약
성능 최적화는 “감”으로 하면 실패하기 쉽습니다. 먼저 측정으로 현재 상태를 숫자로 확인하고, 그 숫자가 무엇을 의미하는지 이해한 뒤, 병목의 종류(드로우콜 과다인지, 폴리곤 과다인지, CPU 업데이트 비용인지)를 좁혀가야 합니다. 이번 회차에서는 BabylonJS에서 가장 빠르게 시작할 수 있는 Inspector와, 코드로 지표를 수집하는 Instrumentation을 함께 사용해 “느린 이유를 재현하고 기록하는 루틴”을 만드는 것이 목표입니다.
목차
- 1) 핵심 포인트(오늘의 결론)
- 2) FPS·프레임타임·드로우콜: 무엇을 먼저 볼까?
- 3) Inspector로 빠르게 확인하는 방법
- 4) Instrumentation으로 숫자 수집하기
- 5) 실습: 오브젝트 수 증가시키며 병목 찾기
- 6) 산출물: 성능 측정 스냅샷 템플릿
- 7) 추가로 생각해볼 점
- 8) 블로그 최적화 정보
1) 핵심 포인트(오늘의 결론)
- FPS만 보지 말고 프레임타임(ms)도 같이 보세요: FPS는 직관적이지만, 원인 분석은 프레임타임이 더 유리합니다.
- 드로우콜(Draw Calls)은 “CPU 제출 비용”과 직결됩니다: 오브젝트/머티리얼이 많아질수록 드로우콜이 늘고, CPU가 먼저 병목이 될 수 있습니다.
- Inspector는 “현장 점검 도구”입니다: 지금 화면에서 무엇이 많은지(메시/머티리얼/텍스처/드로우콜)를 빠르게 확인합니다.
- Instrumentation은 “기록 도구”입니다: 숫자를 코드로 수집해 스냅샷을 남기고, 변경 전/후 비교가 가능해집니다.
- 실습은 재현 가능해야 합니다: 오브젝트 수를 단계적으로 늘리며 “어느 구간부터 무엇이 급증하는지”를 관찰합니다.
2) FPS·프레임타임·드로우콜: 무엇을 먼저 볼까?
2-1) FPS와 프레임타임의 관계
FPS는 “1초에 몇 프레임을 그렸는가”이고, 프레임타임은 “1프레임을 그리는 데 걸린 시간(ms)”입니다. 둘은 반비례 관계입니다. 예를 들어 60FPS는 대략 16.67ms/프레임 수준입니다.
- 60 FPS ≈ 16.67 ms
- 30 FPS ≈ 33.33 ms
- 20 FPS ≈ 50.00 ms
실무적으로는 “목표 FPS”를 잡고, 목표 프레임타임을 기준으로 초과하는 요소를 찾는 방식이 많이 쓰입니다.
2-2) 드로우콜(Draw Calls)이 중요한 이유
드로우콜은 GPU가 화면에 그리기 위해 “그려라”라고 명령을 내리는 호출 단위로 이해하셔도 충분합니다. 드로우콜이 많아지면 GPU가 아니라 CPU(렌더 제출, 상태 변경)가 먼저 바빠질 수 있습니다. 특히 오브젝트가 많고 머티리얼이 다양할수록 드로우콜이 늘어나기 쉽습니다.
2-3) 초보자 기준 ‘우선순위’ 추천
| 우선순위 | 지표 | 의미 | 초보자용 해석 |
|---|---|---|---|
| 1 | FPS / Frame Time | 전체 속도 | 느린지 빠른지 ‘현상’ 확인 |
| 2 | Draw Calls | 렌더 제출량 | 오브젝트/머티리얼이 너무 많지 않은지 |
| 3 | Active Meshes / Total Vertices | 화면에 관여하는 지오메트리 양 | 폴리곤/메시가 과한지 |
3) Inspector로 빠르게 확인하는 방법
Inspector는 “지금 씬에서 무엇이 많은지”를 UI로 보여주는 도구입니다. 초보자에게 특히 좋은 이유는, 코드 수정 없이도 현재 상태를 빠르게 점검할 수 있기 때문입니다.
3-1) Inspector 포함(개발 중에만)
브라우저에서 Inspector를 열려면 Inspector 스크립트를 포함하고, 단축키 또는 버튼으로 호출할 수 있게 만드는 것이 편합니다.
- 개발 중: Inspector 포함
- 배포/릴리스: Inspector 제외(용량/보안/성능 이유)
3-2) Inspector에서 초보자가 먼저 볼 것
- Stats/Performance 영역: FPS, 드로우콜, 메모리/렌더 정보가 함께 보이는 경우가 많습니다.
- Meshes/Materials 목록: 객체/재질이 과도하게 쪼개져 있는지 확인합니다.
- Textures: 텍스처 개수가 많거나 해상도가 과도한지 점검합니다.
Inspector는 “원인 후보를 좁히는 도구”입니다. 이후에는 Instrumentation으로 숫자를 수집해 스냅샷을 남기면 비교가 쉬워집니다.
4) Instrumentation으로 숫자 수집하기
Instrumentation은 “프레임마다 어떤 비용이 발생했는지”를 숫자로 읽을 수 있게 해줍니다. 이번 회차에서는 초보자에게 가장 실용적인 범위로 다음을 다룹니다.
- FPS:
engine.getFps() - 드로우콜: EngineInstrumentation의 drawCallsCounter(가능할 때 사용)
- 액티브 메시/버텍스:
scene.getActiveMeshes().length,scene.getTotalVertices()
중요한 태도는 하나입니다. “한 번에 많은 지표를 다 보려고 하지 말고, 의심되는 병목을 한두 개로 좁혀서 확인”하는 방식이 효율적입니다.
5) 실습: 오브젝트 수 증가시키며 병목 찾기
이 실습의 핵심은 “재현 가능한 부하”를 만드는 것입니다. 박스(또는 구) 메시를 단계적으로 늘려가며 FPS와 드로우콜이 어떻게 변하는지 관찰합니다. 그리고 어느 순간부터 급격히 떨어지는 구간을 찾아 “무엇이 먼저 한계에 도달하는지”를 확인합니다.
5-1) 실습 시나리오
- 버튼으로 박스를 100개씩 추가합니다.
- 매 프레임 HUD에 FPS/오브젝트 수/드로우콜/버텍스를 표시합니다.
- Inspector 토글 버튼을 추가해 현장 점검이 가능하게 만듭니다.
- 스냅샷 버튼으로 현재 수치를 표에 기록해 “성능 측정 스냅샷”을 완성합니다.
5-2) 완성 코드: 측정 HUD + 오브젝트 증가 + 스냅샷 기록
아래 코드는 그대로 index.html로 실행할 수 있습니다(정적 서버 권장). Inspector는 개발 편의용으로 포함되어 있으며, 필요 시 제거해도 실습은 가능합니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BabylonJS Performance Measure (FPS / Draw Calls)</title>
<style>
html, body { width:100%; height:100%; margin:0; overflow:hidden; font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
#renderCanvas { width:100%; height:100%; display:block; touch-action:none; }
#hud {
position: fixed; left: 12px; top: 12px;
width: min(420px, 92vw);
padding: 12px 12px 10px;
border-radius: 14px;
background: rgba(0,0,0,0.55);
border: 1px solid rgba(255,255,255,0.18);
color: rgba(255,255,255,0.92);
z-index: 10;
}
#hudTitle { margin:0 0 8px 0; font-size:14px; font-weight:700; }
#stats { font-size:12px; line-height:1.55; }
.row { display:flex; justify-content:space-between; gap:10px; }
.k { color: rgba(255,255,255,0.75); }
.v { font-variant-numeric: tabular-nums; }
#btns { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
button {
appearance:none; border:1px solid rgba(255,255,255,0.22);
background: rgba(255,255,255,0.10);
color: rgba(255,255,255,0.92);
padding: 7px 10px; border-radius: 10px;
font-size:12px; cursor:pointer;
}
button:hover { background: rgba(255,255,255,0.16); }
#snapWrap {
position: fixed; left: 12px; bottom: 12px;
width: min(720px, 96vw);
padding: 10px 12px;
border-radius: 14px;
background: rgba(0,0,0,0.45);
border: 1px solid rgba(255,255,255,0.14);
color: rgba(255,255,255,0.92);
z-index: 10;
overflow:auto;
max-height: 38vh;
}
#snapWrap h3 { margin:0 0 8px 0; font-size:13px; }
table { width:100%; border-collapse:collapse; font-size:12px; }
th, td { border:1px solid rgba(255,255,255,0.18); padding:6px 8px; text-align:left; }
th { color: rgba(255,255,255,0.85); background: rgba(255,255,255,0.06); }
</style>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<!-- Inspector는 개발 중에만 권장(배포 시 제거) -->
<script src="https://cdn.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
</head>
<body>
<div id="hud">
<p id="hudTitle">성능 측정 HUD (FPS / Draw Calls)</p>
<div id="stats">
<div class="row"><span class="k">FPS</span><span class="v" id="fps">-</span></div>
<div class="row"><span class="k">Frame Time(ms)</span><span class="v" id="ft">-</span></div>
<div class="row"><span class="k">Draw Calls</span><span class="v" id="dc">-</span></div>
<div class="row"><span class="k">Meshes(전체)</span><span class="v" id="mesh">-</span></div>
<div class="row"><span class="k">Active Meshes</span><span class="v" id="active">-</span></div>
<div class="row"><span class="k">Total Vertices</span><span class="v" id="vert">-</span></div>
<div class="row"><span class="k">Boxes(추가한 개수)</span><span class="v" id="count">0</span></div>
</div>
<div id="btns">
<button id="add100">+100 박스</button>
<button id="reset">초기화</button>
<button id="snapshot">스냅샷 기록</button>
<button id="toggleInspector">Inspector 토글</button>
</div>
</div>
<div id="snapWrap">
<h3>성능 측정 스냅샷(기록)</h3>
<table>
<thead>
<tr>
<th>No</th>
<th>Boxes</th>
<th>FPS</th>
<th>FrameTime(ms)</th>
<th>DrawCalls</th>
<th>ActiveMeshes</th>
<th>Vertices</th>
<th>메모(관찰)</th>
</tr>
</thead>
<tbody id="snapBody"></tbody>
</table>
</div>
<canvas id="renderCanvas"></canvas>
<script>
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const ui = {
fps: document.getElementById("fps"),
ft: document.getElementById("ft"),
dc: document.getElementById("dc"),
mesh: document.getElementById("mesh"),
active: document.getElementById("active"),
vert: document.getElementById("vert"),
count: document.getElementById("count"),
snapBody: document.getElementById("snapBody")
};
let scene;
let boxes = [];
let boxCount = 0;
let snapNo = 0;
// Instrumentation(가능하면 사용)
let engineInstr = null;
function tryEnableInstrumentation() {
try {
engineInstr = new BABYLON.EngineInstrumentation(engine);
engineInstr.captureGPUFrameTime = false; // 초보 단계에서는 off(환경 의존)
} catch (e) {
engineInstr = null;
}
}
function createScene() {
const s = new BABYLON.Scene(engine);
s.clearColor = new BABYLON.Color4(0.06, 0.07, 0.09, 1.0);
const camera = new BABYLON.ArcRotateCamera(
"camera",
Math.PI / 2,
Math.PI / 2.4,
18,
new BABYLON.Vector3(0, 2, 0),
s
);
camera.attachControl(canvas, true);
camera.wheelDeltaPercentage = 0.01;
const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), s);
light.intensity = 1.0;
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 60, height: 60 }, s);
ground.position.y = 0;
ground.isPickable = false;
const gmat = new BABYLON.StandardMaterial("gmat", s);
gmat.alpha = 0.18;
ground.material = gmat;
return s;
}
function addBoxes(n) {
// 박스가 늘어날수록 draw calls/active meshes가 늘어나는 것을 관찰하기 위한 단순 부하
// (실무에서는 instancing/thin instances로 개선할 수 있지만, 지금은 측정이 목표)
const s = scene;
const baseMat = new BABYLON.StandardMaterial("baseMat", s);
baseMat.diffuseColor = new BABYLON.Color3(0.75, 0.75, 0.85);
for (let i = 0; i < n; i++) {
const b = BABYLON.MeshBuilder.CreateBox("box_" + (boxCount + i), { size: 0.7 }, s);
b.position.x = (Math.random() - 0.5) * 40;
b.position.z = (Math.random() - 0.5) * 40;
b.position.y = 0.35;
// 일부러 재질을 다양하게 만들어 draw call이 늘어나는 케이스도 관찰할 수 있게 옵션을 남김
// 초보자는 우선 동일 재질로 시작하는 편이 이해가 쉽습니다.
b.material = baseMat;
boxes.push(b);
}
boxCount += n;
ui.count.textContent = String(boxCount);
}
function resetBoxes() {
boxes.forEach(m => m.dispose());
boxes = [];
boxCount = 0;
ui.count.textContent = "0";
}
function getDrawCallsSafe() {
// BabylonJS 버전에 따라 접근 방식이 다를 수 있으므로 안전하게 처리
if (engineInstr && engineInstr.drawCallsCounter && typeof engineInstr.drawCallsCounter.current === "number") {
return engineInstr.drawCallsCounter.current;
}
// 폴백: 알 수 없음
return null;
}
function updateHUD() {
const fps = engine.getFps();
const frameTime = fps > 0 ? (1000 / fps) : 0;
ui.fps.textContent = fps.toFixed(1);
ui.ft.textContent = frameTime.toFixed(2);
const dc = getDrawCallsSafe();
ui.dc.textContent = (dc === null) ? "(inspector로 확인)" : String(dc);
ui.mesh.textContent = String(scene.meshes.length);
ui.active.textContent = String(scene.getActiveMeshes().length);
ui.vert.textContent = String(scene.getTotalVertices());
}
function addSnapshot() {
snapNo += 1;
const fps = parseFloat(ui.fps.textContent) || 0;
const ft = parseFloat(ui.ft.textContent) || 0;
const dcText = ui.dc.textContent;
const dc = (dcText === "(inspector로 확인)") ? "N/A" : dcText;
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${snapNo}</td>
<td>${boxCount}</td>
<td>${fps.toFixed(1)}</td>
<td>${ft.toFixed(2)}</td>
<td>${dc}</td>
<td>${scene.getActiveMeshes().length}</td>
<td>${scene.getTotalVertices()}</td>
<td><span contenteditable="true" style="display:inline-block; min-width:160px; outline:none;">관찰 내용을 적으세요</span></td>
`;
ui.snapBody.appendChild(tr);
}
// 버튼 바인딩
document.getElementById("add100").addEventListener("click", () => addBoxes(100));
document.getElementById("reset").addEventListener("click", () => resetBoxes());
document.getElementById("snapshot").addEventListener("click", () => addSnapshot());
document.getElementById("toggleInspector").addEventListener("click", async () => {
if (!scene) return;
if (scene.debugLayer.isVisible()) {
scene.debugLayer.hide();
} else {
scene.debugLayer.show({ overlay: true });
}
});
// 시작
scene = createScene();
tryEnableInstrumentation();
// 초기 부하(원하면 0으로 시작해도 됨)
addBoxes(200);
engine.runRenderLoop(() => {
scene.render();
updateHUD();
});
window.addEventListener("resize", () => engine.resize());
</script>
</body>
</html>
5-3) 실습 진행 방법(초보자용)
- 1단계: 실행 직후 FPS와 Frame Time을 확인합니다. 이 값이 “기준선”입니다.
- 2단계:
+100 박스를 3~5번 눌러 오브젝트를 늘립니다. 어느 구간부터 FPS가 급격히 떨어지는지 관찰합니다. - 3단계:
스냅샷 기록을 각 구간(예: 200/400/600/800개)에서 눌러 표에 남깁니다. - 4단계: Inspector를 열어 Draw Calls(드로우콜)과 Mesh/Material 수를 확인합니다. 드로우콜이 함께 증가한다면 “제출량 병목” 가능성이 큽니다.
- 5단계: 표의 메모 칸에 “언제부터 체감이 나빠졌는지”, “드로우콜이 얼마나 늘었는지”를 기록합니다.
5-4) 병목을 해석하는 간단한 기준
- 오브젝트 수가 늘수록 FPS가 빠르게 하락하고, 드로우콜도 같이 증가: 드로우콜/CPU 제출 병목 가능성이 큽니다.
- 드로우콜은 크게 늘지 않는데 FPS가 떨어짐: 폴리곤(Vertices)/셰이더/후처리/해상도(픽셀 비용) 문제일 수 있습니다.
- Active Meshes가 비정상적으로 많음: 카메라 밖 오브젝트까지 계속 처리 중인지(컬링/레이어/활성화) 점검합니다.
6) 산출물: 성능 측정 스냅샷 템플릿
“성능 측정 스냅샷”은 최적화를 시작하기 전에 반드시 남겨두면 좋은 기록입니다. 다음 회차에서 최적화를 하더라도, 스냅샷이 있으면 “정말 좋아졌는지”를 숫자로 증명할 수 있습니다.
6-1) 스냅샷에 꼭 포함할 항목
| 항목 | 의미 | 추천 기록 방식 | 비고 |
|---|---|---|---|
| 장면 조건 | 비교의 기준 | 오브젝트 수/해상도/카메라 위치 | 조건이 바뀌면 비교가 무의미해집니다 |
| FPS / Frame Time | 체감 속도 | HUD 숫자, Inspector 수치 | 가능하면 평균값(몇 초 관찰 후) 권장 |
| Draw Calls | 렌더 제출량 | Inspector/Instrumentation | 버전/환경에 따라 표기 위치가 다를 수 있음 |
| Active Meshes / Vertices | 지오메트리 규모 | HUD/콘솔 기록 | 폴리곤 비용 추정에 도움 |
| 관찰 메모 | 정성 기록 | “어느 버튼부터 버벅임” 등 | 최적화 우선순위 판단에 도움 |
6-2) 팀/개인 레포에 남길 최소 문서 형태(예시)
# Performance Snapshot
* Device: (예: Desktop / Laptop / Mobile)
* Browser: (예: Chrome 버전)
* Resolution: (예: 1920x1080)
* Scene: (예: box-stress-test)
* Notes: (예: Inspector on/off 여부)
| Boxes | FPS | FrameTime(ms) | DrawCalls | ActiveMeshes | Vertices | Memo | |
| ----: | --: | ------------: | --------: | -----------: | -------: | --------------- | ------- |
| 200 | 60 | 16.7 | N/A | 210 | 123456 | 기준선 | |
| 400 | 48 | 20.8 | N/A | 410 | 246912 | 마우스 회전 시 약간 둔해짐 | |
7) 추가로 생각해볼 점
- 측정 후에야 최적화가 의미가 있습니다: “드로우콜이 문제인지”도 모르고 머지를 하거나 텍스처를 줄이면, 노력 대비 효과가 작을 수 있습니다.
- Inspector는 개발 중 상시 켜두기보다 필요할 때 켜는 편이 좋습니다: 디버그 레이어는 자체 비용이 있을 수 있으므로, 측정 시에는 Inspector on/off를 구분해 기록하는 습관이 좋습니다.
- 동일 조건 비교가 중요합니다: 카메라 위치, 화면 해상도, 오브젝트 수가 바뀌면 수치가 크게 바뀝니다. 스냅샷에는 조건을 반드시 적어두세요.
- 다음 단계의 최적화 후보: 인스턴싱(Instances/Thin Instances), 메시 머지, 머티리얼 공유, 컬링/LOD, 해상도 스케일링(엔진 하드웨어 스케일) 등이 있습니다. 다만 그 전에 “병목의 종류”부터 확정하는 것이 우선입니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글