Web 3D Engine · 2026

3D를 위한
Babylon.js
완전 학습 가이드

WebGL·WebGPU 기반의 오픈소스 3D 엔진. 기초 씬 구성부터 GLSL Node Material, 수만 개의 Thin Instance 렌더링, Inspector 디버깅 루틴까지 — 초·중·고급 + 이론 + 도구로 정리한 실무 가이드.

사용법
위 카드를 눌러 원하는 레벨로 바로 진입하세요. 진입 후에는 좌우 스와이프 / 하단 화살표 / 하단 레벨 탭으로 자유롭게 이동할 수 있습니다.
Page 01 · 초급

Babylon.js란 무엇인가

Microsoft가 후원하는 오픈소스 JavaScript/TypeScript 3D 엔진. WebGL과 WebGPU 위에서 동작하며, 게임·시각화·XR 경험을 웹 브라우저에서 구현할 수 있습니다.

핵심 특징

다른 엔진과의 비교

Babylon.js

  • 문서가 체계적
  • Node Material 강력
  • 기본 기능이 풍부
  • Playground 생태계

Three.js

  • 경량·유연
  • 생태계가 큼
  • 구성을 직접 해야 함
  • 커뮤니티 중심
Page 02 · 초급

설치와 첫 Scene 만들기

npm 설치 후 Engine → Scene → Camera → Light → Mesh 순으로 구성하는 것이 기본 흐름입니다.

패키지 설치

# 코어 + 로더 + GUI
npm install @babylonjs/core @babylonjs/loaders @babylonjs/gui

최소 동작 예제 (TypeScript)

import {
  Engine, Scene, ArcRotateCamera,
  HemisphericLight, MeshBuilder, Vector3
} from "@babylonjs/core";

const canvas = document.getElementById("c") as HTMLCanvasElement;
const engine = new Engine(canvas, true);
const scene  = new Scene(engine);

const camera = new ArcRotateCamera(
  "cam", -Math.PI/2, Math.PI/2.5,
  5, Vector3.Zero(), scene
);
camera.attachControl(canvas, true);

new HemisphericLight("h", new Vector3(0,1,0), scene);
MeshBuilder.CreateBox("box", { size: 1 }, scene);

engine.runRenderLoop(() => scene.render());
window.addEventListener("resize", () => engine.resize());
Page 03 · 초급

핵심 빌딩 블록

Babylon.js의 모든 씬은 아래 5가지 요소의 조합으로 구성됩니다.

01Engine
02Scene
03Camera
04Light
05Mesh
Engine
WebGL/WebGPU 컨텍스트를 감싸는 최상위 객체. Canvas와 1:1 매칭됩니다.
Scene
3D 공간 자체. 모든 카메라·라이트·메쉬의 컨테이너 역할.
Camera
시점. ArcRotateCamera(궤도), FreeCamera(FPS), UniversalCamera 등.
Light
Hemispheric(전역), Directional(태양광), Point(전구), Spot(스포트) 네 종류.
Mesh
실제 렌더링되는 기하 객체. 정점·인덱스·머티리얼을 가집니다.

좌표계 · 단위

Babylon.js는 왼손 좌표계(LH)가 기본이지만 scene.useRightHandedSystem = true로 glTF 호환을 위해 오른손으로 전환할 수 있습니다. 단위는 통상 1 unit = 1 meter로 다룹니다.

Page 04 · 초급

기본 도형과 간단한 인터랙션

MeshBuilder로 표준 도형을 만들고, ActionManager나 pointer 이벤트로 클릭·호버를 처리합니다.

자주 쓰는 MeshBuilder

MeshBuilder.CreateBox     ("b", { size: 1 }, scene);
MeshBuilder.CreateSphere  ("s", { diameter: 1, segments: 32 }, scene);
MeshBuilder.CreateGround  ("g", { width: 10, height: 10 }, scene);
MeshBuilder.CreateCylinder("c", { diameter: 1, height: 2 }, scene);

클릭 이벤트

import { ActionManager, ExecuteCodeAction } from "@babylonjs/core";

box.actionManager = new ActionManager(scene);
box.actionManager.registerAction(
  new ExecuteCodeAction(
    ActionManager.OnPickTrigger,
    () => { box.scaling.scaleInPlace(1.1); }
  )
);
프레임 루프 매 프레임 실행할 로직은 scene.onBeforeRenderObservable.add(cb)에 등록합니다. engine.getDeltaTime()로 프레임 간격(ms)을 얻어 시간 기반 애니메이션을 만드세요.
Page 05 · 초급

Vue 3 컴포넌트로 래핑하기

Composition API + <script setup> 패턴으로 생명주기에 맞춰 엔진을 안전하게 생성/폐기합니다.

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import {
  Engine, Scene, ArcRotateCamera,
  HemisphericLight, MeshBuilder, Vector3
} from "@babylonjs/core";

const canvasRef = ref<HTMLCanvasElement>();
let engine: Engine | null = null;

onMounted(() => {
  engine = new Engine(canvasRef.value!, true);
  const scene = new Scene(engine);
  const cam = new ArcRotateCamera(
    "c",-Math.PI/2, Math.PI/2.5,5,
    Vector3.Zero(), scene
  );
  cam.attachControl(canvasRef.value!, true);
  new HemisphericLight("h", Vector3.Up(), scene);
  MeshBuilder.CreateBox("b", {}, scene);
  engine.runRenderLoop(() => scene.render());
});

onBeforeUnmount(() => engine?.dispose());
</script>

<template>
  <canvas ref="canvasRef" style="width:100%;height:100%" />
</template>
주의 컴포넌트가 언마운트될 때 engine.dispose()를 반드시 호출하세요. 안 그러면 WebGL 컨텍스트 누수로 메모리가 계속 증가합니다.
Page 06 · 초급 정리

초급 체크리스트

여기까지 편하게 이해됐다면 중급으로 넘어갈 준비가 완료된 겁니다.

내 것으로 만드는 과제
  1. 박스·구·그라운드를 각각 하나씩 배치하고 카메라로 궤도 회전시키기
  2. 구를 클릭하면 색이 바뀌도록 ActionManager로 구현
  3. 프레임 루프에서 박스가 초당 90° 회전하도록 델타 타임 적용
  4. Vue 컴포넌트로 래핑하고 props로 박스 크기를 받아 반영

다음 단계에서 다룰 것

다음 레벨 상단 중급 탭을 눌러 머티리얼·셰이더 파트로 이동하거나, 오른쪽 화살표로 순서대로 이어서 보세요.
Page 07 · 중급

머티리얼 — 객체에 색을 부여하는 방법

메쉬가 어떻게 빛에 반응하고 어떤 색·질감으로 보일지 결정하는 것이 Material입니다. Babylon.js는 4가지 접근을 제공합니다.

선택 가이드

StandardMaterial
레거시 Phong 기반. 가볍고 빠름. 간단한 단색/텍스처에 적합.
PBRMaterial
물리 기반 렌더링(금속성·거칠기). 사실적 비주얼의 표준.
NodeMaterial
노드 에디터로 셰이더를 시각적으로 구성. 커스텀 이펙트의 주력.
ShaderMaterial
GLSL을 직접 작성. 최대한의 자유도, 최대한의 책임.

StandardMaterial 빠른 적용

import { StandardMaterial, Color3, Texture } from "@babylonjs/core";

const mat = new StandardMaterial("m", scene);
mat.diffuseColor  = new Color3(0.75, 0.29, 1);  // 본체 색
mat.specularColor = Color3.Black();                 // 하이라이트 제거
mat.emissiveColor = new Color3(0.05, 0, 0.1);    // 자체 발광
mat.diffuseTexture = new Texture("/tex/wood.jpg", scene);
box.material = mat;
Page 08 · 중급

PBRMaterial — 사실적인 질감

금속성(metallic)과 거칠기(roughness)라는 두 축으로 거의 모든 실물 재질을 표현할 수 있습니다.

import { PBRMaterial, Color3, Texture } from "@babylonjs/core";

const pbr = new PBRMaterial("pbr", scene);
pbr.albedoColor     = Color3.FromHexString("#bf4aff");
pbr.metallic        = 0.9;   // 0 = 비금속, 1 = 금속
pbr.roughness       = 0.15;  // 0 = 거울, 1 = 완전 무광
pbr.environmentIntensity = 1.2;

// 환경 IBL (반사를 위해 권장)
scene.environmentTexture = CubeTexture.CreateFromPrefilteredData(
  "/env/studio.env", scene
);

sphere.material = pbr;

왜 PBR인가

성능 메모 PBR은 Standard보다 프래그먼트 셰이더 비용이 큽니다. 모바일을 타깃한다면 IBL 해상도를 낮추고 forceIrradianceInFragment를 비활성화하는 등의 최적화를 고려하세요.
Page 09 · 중급

Node Material Editor

셰이더를 코드가 아니라 노드 그래프로 설계하는 도구. 결과는 그대로 Babylon 씬에 저장·로드됩니다.

에디터 접근

nme.babylonjs.com에서 브라우저로 바로 실행. 상단의 Save를 눌러 .json으로 저장하면 그대로 Babylon에서 불러올 수 있습니다.

주의 Node Material 그래프·텍스처·샘플 에셋에는 저작권/라이선스가 적용될 수 있습니다. 팀·상용 프로젝트에서는 사용 전 라이선스 조항을 반드시 확인하세요.
PositionWorldPosVP * WorldVertexOutput
UVTextureLerpFragmentOutput
TimeSinMultiply

JSON으로 로드하기

import { NodeMaterial } from "@babylonjs/core";

const nodeMat = await NodeMaterial.ParseFromFileAsync(
  "myGlow",
  "/materials/glow.json",
  scene
);
mesh.material = nodeMat;

// 런타임에 파라미터 바꾸기
const timeBlock = nodeMat.getBlockByName("Time");
// 또는 Input block 값 변경
const colorInput = nodeMat.getInputBlockByPredicate(
  b => b.name === "tint"
);
colorInput!.value = new Color3(1, 0.3, 0.7);
장점 Node Material은 내부적으로 GLSL과 WGSL(WebGPU) 코드를 자동 생성합니다. 즉, 하나의 그래프가 두 API 모두에서 동작한다는 뜻입니다.
Page 10 · 중색

Node Material 실전 패턴

자주 쓰이는 3가지 패턴 — 프로시저럴 그라디언트, 프레넬 림 라이트, UV 스크롤.

1. 그라디언트 (두 색 혼합)

Position.yRemap(-1,1 → 0,1)Lerp(colorA, colorB, t)Fragment

2. 프레넬 (외곽 하이라이트)

ViewDirDot(Normal)OneMinusPow(2.5)Emissive

3. UV 스크롤 (흐르는 텍스처)

UVAddTime × Speed
Texture.SampleFragment
실전 조합 PBR 결과 + 프레넬 Emissive를 Add로 합치면 “가장자리만 빛나는 홀로그램” 효과가 됩니다. 게임 UI나 하이라이트 표현에 자주 쓰이는 레시피입니다.
Page 11 · 중급

GLSL 직접 쓰기 — ShaderMaterial

완전한 제어가 필요할 때는 정점 셰이더와 프래그먼트 셰이더를 직접 작성합니다.

// vertex.glsl
precision highp float;
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
uniform   mat4 worldViewProjection;
varying   vec2 vUV;
varying   vec3 vN;
void main() {
  vUV = uv; vN = normal;
  gl_Position = worldViewProjection * vec4(position, 1.0);
}
// fragment.glsl
precision highp float;
uniform float time;
varying vec2  vUV;
void main() {
  vec3 c = 0.5 + 0.5 * cos(time + vUV.xyx + vec3(0,2,4));
  gl_FragColor = vec4(c, 1.0);
}
import { ShaderMaterial, Effect } from "@babylonjs/core";

Effect.ShadersStore["myVertexShader"]   = vertexSrc;
Effect.ShadersStore["myFragmentShader"] = fragSrc;

const mat = new ShaderMaterial("my", scene,
  { vertex: "my", fragment: "my" },
  {
    attributes: ["position", "normal", "uv"],
    uniforms:   ["worldViewProjection", "time"]
  }
);

let t = 0;
scene.onBeforeRenderObservable.add(() => {
  t += engine.getDeltaTime() / 1000;
  mat.setFloat("time", t);
});
Node vs Shader 새 프로젝트라면 Node Material이 기본입니다. ShaderMaterial은 극한 최적화나 기존 GLSL 자산이 있을 때만 선택하세요. WebGPU 전환 시 WGSL로 다시 써야 할 위험이 있습니다.
Page 12 · 중급 정리

언제 무엇을 쓸 것인가

단색 · 텍스처 · UI

  • StandardMaterial
  • 가벼움
  • 모바일 친화

사실적 비주얼

  • PBRMaterial
  • glTF 호환
  • IBL 필수

커스텀 이펙트

  • NodeMaterial
  • GLSL/WGSL 자동
  • 아티스트 친화

극한 커스텀

  • ShaderMaterial
  • 최대 자유도
  • 유지보수 부담
체크리스트
  1. Standard/PBR/Node/Shader 중 쓸 것을 상황별로 선택할 수 있다
  2. Node Material Editor로 프레넬 이펙트를 만들 수 있다
  3. ShaderMaterial에 uniform을 넘기고 매 프레임 갱신할 수 있다
다음 레벨 이제 씬에 수만 개의 객체를 띄워야 한다면? — 고급 파트에서 Thin Instance로 해결합니다.
Page 13 · 고급

Thin Instance — 왜 필요한가

같은 메쉬를 수천·수만 개 그려야 할 때(풀잎, 별, 파티클, 복셀, 건물 타일…) 각 객체를 일반 Mesh로 만들면 드로우콜과 메모리가 폭발합니다.

세 가지 선택지

Mesh.clone()
완전 복제. 각자 변환·머티리얼 독립. 가장 무거움.
Mesh.createInstance()
GPU 인스턴싱. 각 인스턴스는 Node로 존재해 JS 오버헤드 존재. 수천 개까지.
Thin Instance
JS Node 없이 변환 행렬만 버퍼로 GPU에 올림. 수만~수십만 개에 적합.

성능 감각

같은 박스 50,000개 렌더 (데스크톱 기준, 참고용):

  Mesh 50,000개       →  프레임 1~3 FPS  (드로우콜 5만)
  Instance 50,000개   →  프레임 30~45 FPS (드로우콜 1, JS 비용 있음)
  Thin Instance 5만   →  프레임 60 FPS     (드로우콜 1, JS 비용 거의 0)
핵심 Thin Instance는 각 인스턴스가 Scene Graph의 Node가 아닙니다. 개별 클릭·충돌이 필요 없거나 수동으로 처리할 수 있을 때 쓰는 최저 오버헤드 인스턴싱입니다.
Page 14 · 고급

Thin Instance 기본 사용법

원본 메쉬 하나에 4×4 변환 행렬 버퍼를 넘기면 끝입니다.

import { MeshBuilder, Matrix } from "@babylonjs/core";

const base = MeshBuilder.CreateBox("b", { size: 0.2 }, scene);

const count = 50000;
const matrices = new Float32Array(count * 16);

for (let i = 0; i < count; i++) {
  const m = Matrix.Translation(
    (Math.random()-0.5) * 50,
    (Math.random()-0.5) * 50,
    (Math.random()-0.5) * 50
  );
  m.copyToArray(matrices, i * 16);
}

// 두 번째 인자 true → GPU staticBuffer로 최적화
base.thinInstanceSetBuffer("matrix", matrices, 16, true);

개별 갱신이 필요하면

// 정적이 아니라 매 프레임 바뀐다면 staticBuffer=false
base.thinInstanceSetBuffer("matrix", matrices, 16, false);

scene.onBeforeRenderObservable.add(() => {
  // 100번째 인스턴스만 회전
  const m = Matrix.RotationY(performance.now() * 0.001);
  m.copyToArray(matrices, 100 * 16);
  base.thinInstanceBufferUpdated("matrix");
});
staticBuffer 선택 한 번 세팅 후 바뀌지 않으면 반드시 true. 매 프레임 변동이 있으면 false. 잘못 선택하면 업데이트가 안 되거나 성능이 하락합니다.
Page 15 · 고급

편의 API와 인스턴스별 색상

직접 Float32Array를 다루지 않아도 되는 헬퍼가 있고, 인스턴스마다 다른 색도 줄 수 있습니다.

편의 메서드

// 개수만 지정해 한 번에 할당 (행렬은 Identity로 초기화)
base.thinInstanceCount = 10000;

// 한 개씩 추가하는 패턴
for (let i = 0; i < 10; i++) {
  const m = Matrix.Translation(i, 0, 0);
  base.thinInstanceAdd(m);      // refresh=true가 기본
}

// 특정 인덱스 덮어쓰기
base.thinInstanceSetMatrixAt(3, Matrix.Scaling(2,2,2));

인스턴스별 색상

const colors = new Float32Array(count * 4);
for (let i = 0; i < count; i++) {
  colors[i*4+0] = Math.random();
  colors[i*4+1] = Math.random();
  colors[i*4+2] = Math.random();
  colors[i*4+3] = 1;
}

base.thinInstanceSetBuffer("color", colors, 4, true);

// StandardMaterial은 vertex color를 쓰도록 알려야 함
(mat as StandardMaterial).diffuseColor = Color3.White();
(mat as any).useVertexColors = true;
커스텀 어트리뷰트 thinInstanceSetBuffer("myAttr", data, stride)로 임의 속성도 넘길 수 있습니다. Node Material에서 InstanceColor 혹은 InstancesBlock으로 받아 셰이더 안에서 자유롭게 활용하세요.
Page 16 · 고급

피킹, 컬링, 그림자

Thin Instance는 Node가 아니기 때문에 일반 기능 몇 가지를 명시적으로 켜줘야 합니다.

개별 피킹 활성화

// 기본적으로 thin instance는 하나의 메쉬로 피킹됨
base.thinInstanceEnablePicking = true;

scene.onPointerDown = () => {
  const pick = scene.pick(scene.pointerX, scene.pointerY);
  if (pick?.pickedMesh === base) {
    console.log("클릭한 인스턴스 인덱스:", pick.thinInstanceIndex);
  }
};

프러스텀 컬링

기본값은 원본 메쉬의 바운딩 박스로 전체가 같이 컬링됩니다. 인스턴스가 넓게 퍼져 있다면 다음 중 하나를 쓰세요.

// 1) 수동으로 큰 바운딩 박스 설정
base.setBoundingInfo(new BoundingInfo(
  new Vector3(-100,-100,-100),
  new Vector3(100, 100, 100)
));

// 2) 인스턴스별 개별 컬링 활성화 (오버헤드 있음)
base.thinInstanceEnablePicking = true;
base.thinInstanceRefreshBoundingInfo(true);
그림자 Thin Instance도 ShadowGenerator.getShadowMap().renderList.push(base)로 섀도우 캐스터에 등록됩니다. 단, 인스턴스가 많을수록 섀도우 맵 렌더 비용도 비례해서 증가하니 그림자 해상도를 낮추거나 CSM을 고려하세요.
Page 17 · 고급 · 실전

종합 예제 — 64,000개 큐브 그리드

지금까지 배운 것을 한 번에: 40×40×40 그리드에 색상 그라디언트를 입힌 큐브를 단 1회 드로우콜로 렌더링합니다. Babylon.js Playground의 대표 Thin Instance 데모를 TypeScript + Vue 3로 옮긴 버전입니다.

createScene 함수 (TypeScript)

import {
  Engine, Scene, ArcRotateCamera, Vector3,
  MeshBuilder, Matrix, StandardMaterial, Color3
} from "@babylonjs/core";

export const createScene = (
  engine: Engine,
  canvas: HTMLCanvasElement
): Scene => {
  const scene = new Scene(engine);

  const camera = new ArcRotateCamera(
    "Camera", -Math.PI / 5, Math.PI / 3, 200,
    Vector3.Zero(), scene
  );
  camera.attachControl(canvas, true);

  const box = MeshBuilder.CreateBox("root", { size: 1 }, scene);
  const numPerSide = 40;
  const size = 100;
  const ofst = size / (numPerSide - 1);
  const m = Matrix.Identity();
  let col = 0, index = 0;
  const instanceCount = numPerSide ** 3; // 64,000
  const matricesData = new Float32Array(16 * instanceCount);
  const colorData    = new Float32Array(4  * instanceCount);

  for (let x = 0; x < numPerSide; x++) {
    m.m[12] = -size / 2 + ofst * x;
    for (let y = 0; y < numPerSide; y++) {
      m.m[13] = -size / 2 + ofst * y;
      for (let z = 0; z < numPerSide; z++) {
        m.m[14] = -size / 2 + ofst * z;
        m.copyToArray(matricesData, index * 16);
        const coli = Math.floor(col);
        colorData[index*4]   = ((coli & 0xff0000) >> 16) / 255;
        colorData[index*4+1] = ((coli & 0x00ff00) >>  8) / 255;
        colorData[index*4+2] = ((coli & 0x0000ff)       ) / 255;
        colorData[index*4+3] = 1.0;
        index++; col += 0xffffff / instanceCount;
      }
    }
  }
  box.thinInstanceSetBuffer("matrix", matricesData, 16);
  box.thinInstanceSetBuffer("color",  colorData,    4);
  const mat = new StandardMaterial("material", scene);
  mat.disableLighting = true;
  mat.emissiveColor   = Color3.White();
  box.material = mat;
  return scene;
};
export default createScene;

Vue 3 컴포넌트에서 쓰기

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Engine } from "@babylonjs/core";
import createScene from "./createScene";
const canvasRef = ref<HTMLCanvasElement>();
let engine: Engine | null = null;
onMounted(() => {
  const canvas = canvasRef.value!;
  engine = new Engine(canvas, true);
  const scene = createScene(engine, canvas);
  engine.runRenderLoop(() => scene.render());
  window.addEventListener("resize", () => engine?.resize());
});
onBeforeUnmount(() => engine?.dispose());
</script>
<template><canvas ref="canvasRef" style="width:100%;height:100vh" /></template>

원본 JS → TS 변환 포인트

var → const/let
재할당되는 변수만 let, 나머지는 const.
BABYLON.*
전역 대신 @babylonjs/core에서 이름 임포트 — 트리쉐이킹에 유리.
BoxBuilder
최신 API는 MeshBuilder.CreateBox로 통합. BoxBuilder는 레거시.
엔진 주입
전역 engine/canvas 대신 파라미터로 받아 테스트·재사용이 쉬워짐.
관전 포인트 staticBuffer 기본값 true → 한 번 업로드 후 GPU 고정. 64,000개 큐브가 드로우콜 1회로 60fps 렌더링. disableLighting + emissiveColor = White 조합으로 인스턴스별 color 버퍼가 그대로 최종 색이 됩니다.
Page 19 · 마무리

실무 체크리스트 & 다음 목적지

Thin Instance 운영 원칙

더 파볼 만한 주제

WebGPU
컴퓨트 셰이더로 인스턴스 변환을 GPU에서 생성하기
GPU Particles
파티클은 Thin Instance보다 전용 시스템이 더 적합한 경우가 많음
LOD
거리별 다른 메쉬를 Thin Instance 세트로 교체
Havok Physics
Thin Instance는 기본적으로 물리 바디가 없음 → 필요 시 수동 동기화
수고하셨습니다 여기까지 따라왔다면 Babylon.js의 실무 수준 뼈대는 거의 잡힌 겁니다. 초급에서 씬을 세웠고, 중급에서 Node Material로 픽셀을 제어했고, 고급에서 수만 개 객체를 60fps로 굴리는 법을 익혔습니다. 이제 실제 프로젝트에서 직접 써볼 차례입니다.
Build something unforgettable. ship it.
이론 A · 수학 기초

행렬과 3D 변환의 기초

3D 공간에서 모든 물체의 위치·방향·크기는 결국 하나의 4×4 행렬로 표현됩니다. Position, Rotation, Scale이 어떻게 행렬로 합쳐지는지 살펴봅니다.

세 가지 변환 요소

Translation (이동)
물체를 X·Y·Z 방향으로 얼마나 이동시킬지. Babylon에서는 mesh.position (Vector3).
Rotation (회전)
물체를 각 축 기준으로 얼마나 돌릴지. mesh.rotation(오일러) 또는 mesh.rotationQuaternion.
Scale (크기)
물체를 각 축 방향으로 얼마나 늘리거나 줄일지. mesh.scaling (Vector3).

4×4 행렬이 필요한 이유

3D 공간은 3개의 축(x, y, z)이 있지만 이동(Translation)을 곱셈으로 표현하려면 4×4 행렬이 필요합니다. 이를 동차 좌표계(Homogeneous Coordinates)라 합니다. 4번째 성분(w)을 1로 설정하면 이동·회전·크기 변환을 모두 하나의 행렬 곱셈으로 처리할 수 있습니다.

[ Sx·Rxx  Rxy   Rxz   Tx ]
[  Ryx   Sy·Ryy Ryz   Ty ]   ←  4×4 변환 행렬
[  Rzx    Rzy  Sz·Rzz Tz ]
[   0      0     0    1  ]
핵심 규칙 최종 변환 행렬 M = T · R · S (이동 × 회전 × 크기). 행렬 곱셈은 오른쪽부터 적용되므로 Scale → Rotate → Translate 순서로 물체에 반영됩니다.

Babylon.js에서 행렬 얻기

import { Matrix } from "@babylonjs/core";

// 메쉬의 월드 변환 행렬 (TRS 합성)
const worldMatrix: Matrix = mesh.getWorldMatrix();

// 직접 TRS 행렬 조합
const t = Matrix.Translation(1, 2, 3);
const r = Matrix.RotationYawPitchRoll(yaw, pitch, roll);
const s = Matrix.Scaling(2, 2, 2);
const m = s.multiply(r).multiply(t); // S→R→T
이론 B · 회전 표현

오일러각 vs 쿼터니언

회전을 표현하는 두 가지 방식의 차이와, 실무에서 쿼터니언을 써야 하는 이유를 짐벌락(Gimbal Lock)을 중심으로 설명합니다.

오일러각 (Euler Angles)

세 축(X·Y·Z)을 순서대로 회전시키는 방식. 직관적이지만 순서에 따라 결과가 달라지며, 특정 상황에서 자유도를 잃는 문제가 있습니다.

장점

  • 사람이 이해하기 쉬움 (pitch 30°, yaw 90° 등)
  • 인스펙터·에디터에서 편집하기 직관적
  • 저장·전송 데이터가 작음 (3개 값)

단점

  • 짐벌락(Gimbal Lock) 발생 가능
  • 두 회전 사이 보간(SLERP)이 부자연스러움
  • 회전 순서(XYZ·ZYX 등)에 따라 결과가 달라짐

짐벌락이란?

예를 들어 Y축을 90° 회전시키면 X축과 Z축이 겹쳐버려 한 축의 자유도가 사라지는 현상입니다. 항공기·카메라 등 연속 회전이 많은 경우에 특히 문제가 됩니다.

Gimbal Lock 발생 조건 Y(Yaw)축을 ±90° 회전하면 X(Pitch)와 Z(Roll)이 동일 평면에 놓여 독립적으로 제어 불가. 실제로 3D 애니메이션·로봇공학에서 치명적 버그를 일으킵니다.

쿼터니언 (Quaternion)

4개의 숫자 (x, y, z, w)로 회전을 표현하는 방식. 임의 축을 중심으로 한 번에 회전을 정의하므로 짐벌락이 수학적으로 불가능합니다.

짐벌락 없음
어떤 각도에서도 세 축이 독립적으로 유지됨.
SLERP 보간
두 회전 사이를 구면 선형 보간(Spherical Linear Interpolation)으로 자연스럽게 전환.
연산 효율
행렬(16개 값)보다 적은 데이터(4개), 연속 곱셈 시 오차 누적도 적음.
import { Quaternion, Vector3 } from "@babylonjs/core";

// 오일러 → 쿼터니언 변환
const q = Quaternion.FromEulerAngles(pitch, yaw, roll);

// SLERP: 두 회전 사이를 0~1 t로 보간
const blended = Quaternion.Slerp(qStart, qEnd, 0.5);

// 메쉬에 적용 (rotationQuaternion 사용 시 rotation 무시됨)
mesh.rotationQuaternion = q;
실무 지침 애니메이션·물리·카메라처럼 연속 회전이 있는 곳은 쿼터니언을 사용하세요. UI 인스펙터처럼 사람이 편집하는 곳에서만 오일러각을 쓰고, 내부에서는 즉시 쿼터니언으로 변환하는 것이 정석입니다.
이론 C · 변환 행렬

변환 행렬 (Transformation Matrix)

Translation·Rotation·Scale 세 요소를 하나로 합친 행렬이 변환 행렬입니다. Thin Instance에서 인스턴스마다 행렬 하나를 GPU에 전달하는 이유가 바로 여기에 있습니다.

변환 행렬 = T × R × S

import { Matrix, Quaternion, Vector3 } from "@babylonjs/core";

const position = new Vector3(3, 0, 1);
const rotation = Quaternion.FromEulerAngles(0, Math.PI / 4, 0); // Y 45°
const scale    = new Vector3(2, 2, 2);

// 세 가지를 하나의 4×4 행렬로 합성
const matrix = Matrix.Compose(scale, rotation, position);

Thin Instance와 변환 행렬

Thin Instance는 인스턴스별로 개별 mesh.position·rotation이 없습니다. 대신 각 인스턴스의 변환 행렬을 Float32Array(16개 값 × 인스턴스 수)로 묶어 GPU에 한꺼번에 전달합니다.

const count = 1000;
const buffer = new Float32Array(16 * count);

for (let i = 0; i < count; i++) {
  const m = Matrix.Compose(
    new Vector3(1, 1, 1),                          // scale
    Quaternion.Identity(),                           // rotation
    new Vector3(i * 2, 0, 0)                        // position
  );
  m.copyToArray(buffer, i * 16);
}

mesh.thinInstanceSetBuffer("matrix", buffer, 16);

로컬 공간 vs 월드 공간

로컬 공간
물체 자신을 기준으로 한 좌표계. Matrix.Compose로 만든 행렬은 보통 로컬 변환.
월드 공간
씬 전체 기준 좌표계. mesh.getWorldMatrix()는 부모 계층까지 포함한 최종 행렬.
부모-자식 계층
자식의 월드 행렬 = 부모 월드 행렬 × 자식 로컬 행렬. Babylon이 자동으로 계산.
정리 변환 행렬 = TRS를 하나로 압축한 언어입니다. 쿼터니언으로 회전을 안전하게 표현하고, Matrix.Compose로 세 요소를 합친 뒤, GPU에 Float32Array로 전달하는 흐름을 이해하면 Thin Instance의 핵심을 파악한 것입니다.
도구 A · Inspector

Babylon Inspector 활용 가이드

Inspector는 씬 구조·머티리얼·라이트·성능 상태를 실시간으로 확인하는 디버깅 도구입니다. 개발 단계에서 가장 먼저 켜야 하는 도구입니다.

열기

import { Inspector } from "@babylonjs/inspector";

scene.debugLayer.show({
  embedMode: true,
  handleResize: true
});
Tip 번들 크기를 줄이려면 개발 모드에서만 Inspector를 동적 import 하세요.

Inspector에서 꼭 볼 항목

실무 팁

권장 루틴 기능 추가 후 Inspector에서 Draw Call과 Active Mesh를 먼저 확인하고, 이상이 있으면 씬 트리와 머티리얼 설정을 역추적하세요.
도구 B · 창 구성 이해

Inspector를 켜면 뜨는 창, 초보자용 해설

처음 Inspector를 열면 패널이 많아 보이지만, 실제로는 “무엇이 그려졌는지 / 왜 느린지 / 어디가 잘못됐는지”를 순서대로 보는 도구입니다.

1) Scene Explorer (왼쪽 트리)

2) Properties / Material / Texture 패널

3) Stats / Debug 패널

FPS
현재 프레임 속도. 떨어지는 순간의 씬 상태를 같이 봐야 원인을 찾기 쉽습니다.
Draw Calls
GPU에게 “그려라”라고 요청한 횟수. 값이 급증하면 병목 가능성이 큽니다.
Active Meshes
이번 프레임에 실제 렌더링 대상이 된 메시 수.
Debug View
Wireframe, Bounding Box, Normals를 켜서 보이지 않는 문제를 시각적으로 추적.
초보자 루틴 Scene Explorer → Properties/Material → Stats 순서로 보면, “안 보임” 문제와 “느림” 문제를 대부분 빠르게 분리할 수 있습니다.
도구 C · 성능 추적

Draw Call / 바인딩 / 박스 추적 실전 루틴

문제가 생겼을 때 “어디서 비용이 터졌는지”를 단계적으로 좁히는 방식입니다. 아래 순서대로 하면 초보자도 재현 가능한 디버깅 루틴을 만들 수 있습니다.

Step 1. Draw Call 급증 확인

Step 2. Material/Texture 바인딩 의심 구간 확인

바인딩(Binding)은 GPU에 파이프라인·텍스처·버퍼를 연결하는 과정입니다. 오브젝트마다 머티리얼이 조금씩 다르면 바인딩 변경이 잦아지고 Draw Call이 증가하기 쉽습니다.

비효율 패턴

  • 메시마다 다른 Material 인스턴스 사용
  • 텍스처를 프레임마다 교체
  • 투명 머티리얼이 과도하게 많음

개선 패턴

  • 공통 Material 재사용
  • 가능하면 Atlas/배치로 텍스처 통합
  • Thin Instance/Instancing으로 묶기

Step 3. Bounding Box / Frustum Culling 검증

Step 4. 기록 기반으로 원인 고정

기능 토글 전후 수치를 팀 문서에 남기면, “체감”이 아닌 수치 중심으로 회귀를 잡을 수 있습니다.

[성능 기록 예시]
기능: 그림자 품질 High
FPS: 58 -> 41
Draw Calls: 145 -> 312
Active Meshes: 90 -> 91
판단: 메시 수 증가는 거의 없음, Draw Call 급증 -> 머티리얼/쉐도우 패스 분기 확인
- / 0
Contact
EMAIL
GITHUB
목차 · Index