BabylonJS 기초 9일차 : 머티리얼 최적화 기본 (draw call, 재질 공유, material merge 아이디어)


BabylonJS 기초 9일차 : 머티리얼 최적화 기본 (draw call, 재질 공유, material merge 아이디어)

한눈에 보는 요약

씬에 오브젝트가 늘어날 때 성능이 떨어지는 이유는 단순히 “폴리곤이 많아서”만이 아닙니다. 특히 머티리얼(Material)이 많아질수록 CPU와 GPU가 처리해야 하는 준비 작업(상태 변경, 셰이더/텍스처 바인딩, 유니폼 업데이트 등)이 늘어나며, 결과적으로 프레임이 흔들릴 수 있습니다.

오늘은 draw call(드로우콜)을 기준으로 렌더링 비용을 이해하고, “같은 재질 공유 vs 각각 재질 생성”을 비교하는 실습을 통해 “재질 최적화 감각”을 잡아보겠습니다. 마지막으로 material merge(재질을 합치는 사고방식)로 이어지는 실무 팁(텍스처 아틀라스/인스턴싱 가능 구조 만들기)까지 정리합니다.

목차


핵심 포인트

  • 드로우콜(draw call)은 “GPU에게 한 번 그려주세요”라고 요청하는 단위

    오브젝트가 많아질수록 드로우콜이 늘고, CPU가 준비해야 할 작업도 증가합니다.

  • 재질이 많으면 “상태 변경 비용”이 커집니다

    텍스처 바인딩, 셰이더(Effect) 선택, 파라미터(유니폼) 업데이트가 반복되어 프레임이 느려질 수 있습니다.

  • “재질 공유”는 드로우콜 숫자를 바로 줄이진 않아도, 준비 비용과 변동성을 줄입니다

    같은 셰이더/텍스처를 반복 사용하면 캐시가 잘 먹고, 재질 수 폭증을 막아 관리·메모리 측면에서도 유리합니다.

  • 드로우콜을 ‘진짜로’ 줄이려면 인스턴싱(Instancing/Thin Instances)이나 메시 병합이 필요

    material merge 아이디어는 결국 “같은 재질로 묶을 수 있게 설계해 인스턴싱/아틀라스를 가능하게 만들자”로 이어집니다.

상세 설명

1) draw call이란 무엇인가

드로우콜(draw call)은 쉽게 말해 “GPU에게 지금 이 메시를 이 재질로 그려주세요”라고 요청하는 한 번의 호출입니다. 일반적으로 메시(정확히는 서브메시)가 많을수록 드로우콜이 늘어납니다.

여기서 중요한 포인트는 드로우콜 자체가 “나쁘다/좋다”가 아니라, 드로우콜이 많아질수록 CPU가 매 프레임 준비해야 하는 일이 늘어난다는 점입니다. 이 준비 작업에는 대략 다음이 포함됩니다.

  • 어떤 셰이더(Effect)를 쓸지 결정하고(또는 캐시에서 가져오고)
  • 재질 파라미터(빛, 색, 러프니스 등)를 유니폼으로 업로드하고
  • 텍스처를 바인딩하고
  • 버텍스/인덱스 버퍼를 설정한 뒤
  • 실제 드로우 호출을 실행합니다

즉, “오브젝트가 1,000개”라면 단순히 1,000번 그리는 것뿐 아니라 1,000번의 준비가 따라올 수 있습니다. 여기에서 최적화의 핵심이 시작됩니다.

2) 재질이 많아질 때 성능이 떨어지는 이유

초보자 입장에서 가장 흔한 패턴은 “조금이라도 다르게 보이게 하려고 재질을 계속 새로 만든다”입니다. 예를 들어 박스 500개를 각기 다른 색으로 칠하려고 박스마다 StandardMaterial을 하나씩 생성하는 식입니다.

이때 성능이 떨어질 수 있는 이유는 다음과 같습니다.

  • 상태 변경(State Change) 비용 증가

    재질이 바뀌면 셰이더/텍스처/유니폼 세팅이 바뀌는 경우가 많고, 이 변경이 반복될수록 CPU 오버헤드가 커질 수 있습니다.

  • 재질(및 텍스처) 객체 수 증가 = 메모리/관리 비용 증가

    재질이 수백 개로 늘면 로딩/해제, 디버깅, 유지보수 난이도가 급격히 올라갑니다. “나중에 전부 톤을 맞추기” 같은 작업이 매우 어려워집니다.

  • 인스턴싱 같은 배칭 기법을 쓰기 어려워짐

    인스턴싱(특히 Thin Instances)은 “같은 메시 + 같은 재질”일 때 빛을 발합니다. 재질이 제각각이면 드로우콜 감소 효과를 얻기 어렵습니다.

여기서 꼭 짚고 넘어갈 점이 하나 있습니다. 재질을 공유한다고 해서 드로우콜 숫자가 자동으로 줄어들지는 않습니다. (메시가 여전히 500개면, 기본적으로 그 500개를 그려야 하니까요.)

그럼에도 재질 공유가 중요한 이유는, 드로우콜 “숫자”가 같아도 매 드로우콜마다 드는 준비 비용이 줄거나(또는 안정화)되고, 무엇보다 다음 단계 최적화(인스턴싱/아틀라스/머지)를 가능하게 해주는 구조를 만들기 때문입니다.

3) material merge 아이디어(“재질을 줄이는 사고방식”)

material merge는 특정 API 한 줄로 해결되는 마법이 아니라, 실무에서 자주 쓰는 “생각의 방향”에 가깝습니다. 핵심은 다음 질문입니다.

“정말로 재질이 달라야만 하는가?” / “같은 재질로 묶어서 그릴 방법은 없는가?”

대표적인 merge 접근은 다음과 같습니다.

  • 색만 다른 경우: 재질을 늘리지 말고 ‘데이터’로 변형

    가능하면 같은 재질을 공유하고, 색 변화는 인스턴스 컬러(Thin Instances 속성)나 버텍스 컬러 같은 방식으로 처리합니다.

  • 텍스처가 많아 재질이 늘어나는 경우: 텍스처 아틀라스(Texture Atlas)

    여러 이미지를 한 장에 모아 UV로 구역을 나누면, 재질/텍스처 바인딩 횟수를 줄일 수 있습니다.

  • 완전히 고정된 오브젝트가 많다면: 메시 병합(Mesh Merge)

    하나의 큰 메시로 합치면 드로우콜이 줄 수 있지만, 컬링/개별 제어가 어려워질 수 있어 상황을 보고 선택합니다.

  • 변형이 거의 없는 재질은 freeze(동결)

    재질 속성을 매 프레임 건드리지 않는다면 material.freeze() 같은 기능으로 비용을 줄일 여지가 있습니다(프로젝트 규모가 커질수록 효과적).


4) 실습: 같은 재질 공유 vs 각각 생성 비교

이번 실습은 “눈으로 확인하는” 방식입니다. 동일한 수의 박스를 만들고, 아래 두 모드를 토글합니다.

  • 모드 A (공유): 박스 전부가 1개의 재질을 공유
  • 모드 B (개별): 박스마다 재질을 새로 생성(색이 다르다는 이유로 재질을 계속 늘리는 상황 재현)

화면 좌측 상단에 FPS / draw calls / materials 개수를 표시해 비교합니다. 보통 관찰 포인트는 다음과 같습니다.

  • 박스 개수가 같다면 draw calls 자체는 크게 달라지지 않을 수 있음(“메시 수” 영향이 큼)
  • 하지만 materials 개수는 개별 모드에서 급증
  • 개별 모드는 프레임 변동이 커지거나 FPS가 더 낮아지는 경향이 나타날 수 있음(환경/기기별 차이는 존재)

코드 예시: 공유 vs 개별 재질 토글 실습

// BabylonJS Day 9: 머티리얼 최적화 기본 실습
// - 1키: 같은 재질 공유(재질 1개)
// - 2키: 박스마다 재질 생성(재질 N개)
// 좌측 상단에 FPS / draw calls / materials 수 표시

const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);

const createScene = () => {
const scene = new BABYLON.Scene(engine);

// 카메라/라이트
const camera = new BABYLON.ArcRotateCamera("cam", -Math.PI / 2, Math.PI / 2.5, 55, new BABYLON.Vector3(0, 0, 0), scene);
camera.attachControl(canvas, true);

const light = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), scene);
light.intensity = 0.9;

// 바닥
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 200, height: 200 }, scene);
const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
groundMat.diffuseColor = new BABYLON.Color3(0.18, 0.18, 0.2);
ground.material = groundMat;

// 성능 계측(가능한 경우 draw calls 카운터)
const instr = new BABYLON.SceneInstrumentation(scene);
try {
// BabylonJS 버전에 따라 속성명이 다를 수 있어 try/catch로 안전 처리
instr.captureDrawCalls = true;
} catch (e) {}

// UI 오버레이
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui", true, scene);
const stats = new BABYLON.GUI.TextBlock("stats", "");
stats.color = "white";
stats.fontSize = 16;
stats.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
stats.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
stats.paddingLeft = 12;
stats.paddingTop = 12;
ui.addControl(stats);

// 실습 파라미터
const COLS = 40;
const ROWS = 25;
const COUNT = COLS * ROWS;
const SPACING = 2.2;

let boxes = [];
let sharedMat = null;
let mode = "SHARED";

const clearBoxesAndMaterials = () => {
// 박스 메시 제거
boxes.forEach((m) => m.dispose());
boxes = [];

```
// 공유 재질 제거
if (sharedMat) {
  sharedMat.dispose();
  sharedMat = null;
}

// 개별 재질이 남아있을 수 있으므로(groundMat 제외) 정리
const toDispose = scene.materials.filter((m) => m !== groundMat);
toDispose.forEach((m) => m.dispose());
```

};

const buildBoxes = (useSharedMaterial) => {
clearBoxesAndMaterials();

```
mode = useSharedMaterial ? "SHARED" : "UNIQUE";

if (useSharedMaterial) {
  sharedMat = new BABYLON.StandardMaterial("sharedMat", scene);
  sharedMat.diffuseColor = new BABYLON.Color3(0.3, 0.6, 0.9);
}

let idx = 0;
for (let r = 0; r < ROWS; r++) {
  for (let c = 0; c < COLS; c++) {
    const box = BABYLON.MeshBuilder.CreateBox("b" + idx, { size: 1.2 }, scene);
    box.position.x = (c - COLS / 2) * SPACING;
    box.position.z = (r - ROWS / 2) * SPACING;
    box.position.y = 0.6;

    if (useSharedMaterial) {
      // 같은 재질을 전부 공유
      box.material = sharedMat;
    } else {
      // 박스마다 재질을 새로 생성(“색만 다르게 보이게 하려고 재질을 늘리는 상황” 재현)
      const m = new BABYLON.StandardMaterial("m" + idx, scene);
      m.diffuseColor = new BABYLON.Color3(Math.random(), Math.random(), Math.random());
      box.material = m;
    }

    boxes.push(box);
    idx++;
  }
}
```

};

// 초기: 공유 모드
buildBoxes(true);

// 키 입력으로 모드 전환
window.addEventListener("keydown", (e) => {
if (e.key === "1") buildBoxes(true);
if (e.key === "2") buildBoxes(false);
});

// 통계 업데이트
scene.onBeforeRenderObservable.add(() => {
const fps = engine.getFps().toFixed(0);

```
// draw calls 가져오기(버전/환경에 따라 접근 경로가 다를 수 있어 안전 처리)
let drawCalls = "-";
try {
  if (instr.drawCallsCounter) drawCalls = String(instr.drawCallsCounter.current);
} catch (e) {}

const materialsCount = scene.materials.length;

stats.text =
  "Day 9 - Material Optimization Lab\n" +
  "Mode: " + mode + " (press 1=SHARED, 2=UNIQUE)\n" +
  "Boxes: " + COUNT + "\n" +
  "Materials: " + materialsCount + "\n" +
  "Draw Calls: " + drawCalls + "\n" +
  "FPS: " + fps;
```

});

return scene;
};

const scene = createScene();
engine.runRenderLoop(() => scene.render());
window.addEventListener("resize", () => engine.resize()); 

이 실습에서 핵심은 “박스 개수는 같은데 재질 수만 바뀐다”는 조건입니다. 2번(UNIQUE) 모드로 바꾸면 scene.materials.length가 급증하는 것을 확인할 수 있고, 기기/브라우저에 따라 FPS 하락이나 프레임 변동이 더 크게 나타날 수 있습니다. 이 차이가 바로 “재질이 많아지면 성능이 흔들리는” 대표적인 출발점입니다.


5) (보너스) 드로우콜을 실제로 줄이는 방법: 인스턴싱/Thin Instances

앞에서 언급했듯, 재질 공유만으로 드로우콜이 자동으로 크게 줄지는 않습니다. 드로우콜을 확 줄이고 싶다면, 같은 메시를 여러 번 찍어내는 구조에서 Instancing 또는 Thin Instances를 고려하는 것이 일반적입니다.

Thin Instances는 “많이 찍어내는 오브젝트”에 특히 강합니다. 같은 메시/같은 재질을 사용하되, 각 인스턴스별로 행렬(위치/회전/스케일)을 넘겨 GPU가 한 번에 처리하도록 돕습니다.

코드 예시: Thin Instances로 “같은 재질, 많은 오브젝트” 그리기

// (보너스) Thin Instances 예시: 드로우콜 감소를 노리는 전형적인 방식
// 같은 메시(박스 1개) + 같은 재질(1개)을 기반으로 수천 개를 GPU 친화적으로 렌더링합니다.

const baseBox = BABYLON.MeshBuilder.CreateBox("baseBox", { size: 1.2 }, scene);
const mat = new BABYLON.StandardMaterial("thinMat", scene);
mat.diffuseColor = new BABYLON.Color3(0.9, 0.55, 0.25);
baseBox.material = mat;

// 기본 메시 자체는 보이지 않게(인스턴스만 보이게)
baseBox.isVisible = false;

const COLS = 80;
const ROWS = 50;
const SPACING = 2.2;

const matrices = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const x = (c - COLS / 2) * SPACING;
const z = (r - ROWS / 2) * SPACING;
const m = BABYLON.Matrix.Translation(x, 0.6, z);
matrices.push(m);
}
}

// Thin Instances 등록
baseBox.thinInstanceAdd(matrices);

// 필요하다면, 인스턴스별 색 같은 속성도 커스텀 버퍼로 줄 수 있습니다(심화 주제). 

정리하면, “재질 공유”는 최적화의 기본 습관이고, “드로우콜 감소”까지 목표로 한다면 인스턴싱/Thin Instances나 메시 병합 같은 배칭 기법을 함께 고려하는 흐름이 좋습니다. 오늘은 그 방향성을 잡는 데 집중하시면 충분합니다.

따라하기

  1. 기본 씬을 만든 뒤, 박스를 충분히 많이 배치합니다.

    COLS/ROWS 값을 늘려 박스 수를 500~2,000 수준으로 올려보세요. 기기마다 체감이 달라, “내 PC/내 모바일에서 어느 지점부터 흔들리는지” 확인하는 것이 중요합니다.

  2. 1번(공유) 모드에서 materials 수와 FPS를 확인합니다.

    materials가 거의 늘지 않는 상태(대략 groundMat + sharedMat 정도)를 기준 화면으로 잡습니다.

  3. 2번(개별) 모드로 바꾸고 materials 수 폭증을 확인합니다.

    박스 개수는 같은데 materials 수가 박스 수에 비례해 늘어납니다. 이때 FPS 하락이나 프레임 변동이 커지면 “재질 수 증가가 부담”이 된다는 신호로 보시면 됩니다.

  4. “왜 재질을 늘렸는지” 원인을 적어봅니다.

    대부분 “색이 달라야 해서”, “텍스처가 달라서”입니다. 다음 단계에서는 색/텍스처 차이를 재질 증가 없이 표현하는 방법(인스턴스 속성, 버텍스 컬러, 아틀라스)을 고민하는 것이 material merge의 출발점입니다.

  5. (선택) Thin Instances 예시로 드로우콜 감소 방향을 확인합니다.

    오브젝트가 “같은 형태가 대량 반복”이라면, 이쪽이 정답인 경우가 많습니다. 단, 개별 메시처럼 각각을 쉽게 클릭/애니메이션/충돌 처리하려면 설계가 달라질 수 있습니다.

비교 표

아래 표는 “재질 최적화”에서 자주 등장하는 선택지를 draw call 관점과 실무 관점으로 정리한 것입니다.

전략 드로우콜 영향 장점 단점/주의 추천 상황
같은 재질 공유 대체로 드로우콜 수 자체는 동일(메시 수 영향이 큼) 상태 변경/관리 비용 감소, 캐시 효율, 유지보수 쉬움 색/텍스처 차이를 재질 없이 처리할 방법이 필요 같은 스타일 오브젝트가 많은 씬 전반
재질을 박스마다 생성 대체로 드로우콜 수는 동일 구현이 가장 단순(각각 마음대로 변경 가능) 재질 수 폭증, 상태 변경 증가, 유지보수/메모리 부담 프로토타입 단계(초기) 외에는 지양
텍스처 아틀라스 재질/텍스처 바인딩 횟수 감소에 유리 여러 텍스처를 1장으로 묶어 재질 수를 줄이기 쉬움 UV 관리 필요, 아틀라스 제작 파이프라인 필요 아이콘/소품 등 텍스처 종류가 많은 경우
메시 병합(Merge) 드로우콜 감소 가능(합친 단위로 그려짐) 대량 정적 오브젝트에서 효과적 개별 컬링/선택/제어가 어려움, 수정 비용 증가 정적 배경 오브젝트가 많을 때
Instancing / Thin Instances 드로우콜 크게 감소 가능(같은 메시/재질 반복에 강함) 대량 반복 렌더링에 최적, 성능 효율 좋음 개별 오브젝트 처리(선택/물리/애니메이션) 설계가 필요 나무, 잔디, 타일, 반복 구조물 등
재질 Freeze 드로우콜 수보다 “프레임당 재질 계산” 비용 감소에 도움 정적인 재질에서 비용 절감, 프레임 안정화 재질 속성을 바꾸려면 해제/재동결 필요 고정 UI/배경/고정 조명 환경의 재질

추가로 생각해볼 점

  • “색이 다르면 재질을 새로 만든다”는 습관부터 점검해 보시면 좋습니다. 색 차이는 재질이 아니라 데이터(인스턴스 컬러/버텍스 컬러)로 해결 가능한 경우가 많습니다.
  • 성능 문제를 볼 때 메시 수(드로우콜)재질 수(상태 변경/관리 비용)를 분리해서 보는 습관이 중요합니다. 둘 중 무엇이 병목인지부터 정리하면 해결 방향이 빨라집니다.
  • 다음 단계(10일차)로는 “텍스처/머티리얼 로딩 전략”, “아틀라스/압축”, “인스턴싱 설계”처럼 ‘실전 파이프라인’ 관점으로 확장하면 학습 흐름이 자연스럽습니다.


이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

Reactions

댓글 쓰기

0 댓글