BabylonJS 27회차 : GUI(2D)와 3D UI
목표: UI를 붙여 “제품 느낌” 만들기
핵심 개념: Babylon GUI, AdvancedDynamicTexture
실습: HUD(체력/좌표) + 3D 버튼 패널
산출물: “UI 포함 인터랙션 씬”
요약
3D 씬이 “기술 데모”에서 “제품”으로 보이기 시작하는 순간은, 대부분 UI가 붙는 순간입니다. 이번 회차에서는 BabylonJS의 GUI 시스템을 두 갈래로 정리합니다. (1) 화면에 고정되는 2D HUD(체력/좌표/상태), (2) 월드 안에 떠 있는 3D UI(버튼/패널). 핵심은 AdvancedDynamicTexture로 “UI가 그려지는 캔버스”를 만들고, 상황에 따라 Fullscreen UI(2D) 또는 GUI3DManager(3D)를 선택해 구성하는 것입니다. 실습에서는 체력 바와 좌표 HUD를 만들고, 3D 버튼 패널로 체력 증감/텔레포트/오토무브 토글을 구현해 “UI 포함 인터랙션 씬”을 완성합니다.
목차
- 1) 핵심 포인트
- 2) Babylon GUI 전체 구조 한 장으로 이해하기
- 3) 2D HUD: Fullscreen AdvancedDynamicTexture
- 4) 3D UI: GUI3DManager와 3D 패널
- 5) 2D/3D UI 선택 기준 표
- 6) 실습: HUD + 3D 버튼 패널 “인터랙션 씬”
- 7) 실행 단계 체크리스트
- 8) 추가로 생각해볼 점
- 9) 블로그 최적화 정보
1) 핵심 포인트
- UI도 “에셋”입니다: 화면의 여백, 정렬, 글꼴 크기, 상태 표현(활성/비활성)이 잡히면 제품 느낌이 크게 올라갑니다.
- AdvancedDynamicTexture(ADT)는 UI가 그려지는 ‘도화지’입니다: Fullscreen UI(2D)든 Mesh UI(3D)든 ADT 개념을 잡아두면 응용이 쉽습니다.
- 2D HUD는 정보 전달에 최적: 체력/미션/좌표/미니맵처럼 “항상 보여야 하는 정보”는 화면 고정이 안정적입니다.
- 3D UI는 상호작용 지점에 최적: 버튼/패널/키오스크/기계 조작처럼 “월드의 특정 위치에서 조작”할 때 설득력이 좋습니다.
- 실습은 작은 기능을 끝까지: HUD로 상태를 보여주고, 3D 버튼으로 상태를 바꾸며, 변화가 HUD에 반영되는 흐름까지 연결합니다.
2) Babylon GUI 전체 구조 한 장으로 이해하기
Babylon GUI는 크게 두 계열로 이해하면 빠릅니다.
| 계열 | 대표 클래스 | 사용 위치 | 핵심 장점 |
|---|---|---|---|
| 2D GUI(화면 오버레이) | AdvancedDynamicTexture.CreateFullscreenUI |
화면 고정 HUD | 가독성, 정렬/레이아웃, 정보 전달 |
| 3D GUI(월드 UI) | GUI3DManager, StackPanel3D, HolographicButton |
월드 내 버튼/패널 | 공간감, 상호작용 지점의 설득력 |
| Mesh UI(3D 표면에 2D UI 붙이기) | AdvancedDynamicTexture.CreateForMesh |
모니터/키오스크/간판 | 2D 레이아웃을 3D 표면에 자연스럽게 투영 |
이번 회차 실습은 Fullscreen UI + GUI3DManager 조합으로 진행합니다. 제품형 데모에서 가장 자주 쓰는 “HUD + 월드 버튼” 패턴이기 때문입니다.
3) 2D HUD: Fullscreen AdvancedDynamicTexture
HUD를 만들 때 초보자가 가장 먼저 잡아야 하는 기준은 “정보의 안정성”입니다. 체력/좌표 같은 정보는 카메라가 어떻게 움직여도 화면에서 사라지면 안 되고, 배경(3D)이 복잡해져도 읽기 쉬워야 합니다.
HUD 구성 기본 규칙(추천)
- 안전 여백: 상단/좌측 12~16px 정도 여백을 두고, 모서리에 딱 붙이지 않습니다.
- 배경 레이어: 반투명 배경(
alpha)을 넣어 어떤 배경에서도 글자가 읽히게 합니다. - 글자 크기 분리: 타이틀(작게 굵게) + 값(숫자 가독성) 패턴으로 구성합니다.
- 상태는 ‘바’로 표현: 체력/게이지는 텍스트보다 바(Progress 형태)가 직관적입니다.
HUD에서 자주 쓰는 컨트롤
Rectangle: 배경 패널, 섹션 카드StackPanel: 세로/가로 정렬TextBlock: 라벨/값 표시Button: 2D 버튼(옵션)
4) 3D UI: GUI3DManager와 3D 패널
3D UI는 “월드에 떠 있는 조작 패널”을 만들 때 유용합니다. 키오스크 버튼, 기계 조작 패널, 상호작용 오브젝트 주변의 툴패널 같은 형태가 대표적입니다.
3D UI를 쓸 때의 장점
- 상호작용 위치가 명확: 사용자는 “어디에서 무엇을 조작하는지”를 자연스럽게 이해합니다.
- 연출이 쉬움: 패널이 특정 오브젝트 옆에 떠 있으면 ‘제품’의 사용 흐름이 살아납니다.
- UI를 씬의 일부로 구성: UI 자체가 오브젝트처럼 보이므로 데모 설득력이 좋아집니다.
3D UI에서 주의할 점
- 가독성: 너무 멀거나, 배경이 복잡하면 읽기 어려워집니다. 크기/거리/대비를 조절해야 합니다.
- 카메라 방향: 패널이 옆으로 돌아가면 버튼이 읽히지 않습니다. 필요하면 카메라를 향하도록(billboard/lookAt) 처리합니다.
- 입력 우선순위: 월드 클릭과 UI 클릭이 섞일 때는 이벤트 흐름을 정리해야 합니다(실습에서는 UI는 3D 버튼, 월드 클릭은 사용하지 않습니다).
5) 2D/3D UI 선택 기준 표
| 상황 | 추천 UI | 이유 | 초보자 팁 |
|---|---|---|---|
| 체력/좌표/미션 | 2D HUD | 항상 보이고 읽기 쉬워야 함 | 반투명 배경 + 간결한 텍스트 |
| 기계 조작 패널 | 3D UI | 월드 위치가 의미를 가짐 | 패널을 카메라 쪽으로 향하게 |
| 모니터/간판/키오스크 화면 | Mesh UI | 2D 레이아웃을 3D 표면에 투영 | CreateForMesh로 시작 |
6) 실습: HUD + 3D 버튼 패널 “인터랙션 씬”
실습에서 구현할 기능은 아래와 같습니다.
- HUD(2D): 체력(바 + 숫자), 플레이어 좌표 표시
- 3D 버튼 패널: Damage(-10), Heal(+10), Teleport(랜덤 이동), AutoMove 토글
- 상호작용 흐름: 3D 버튼 → 상태 변경 → HUD 반영
6-1) 완성 코드: UI 포함 인터랙션 씬
아래 코드는 단일 index.html로 실행 가능합니다(정적 서버 권장). Babylon GUI를 사용하기 위해 babylon.gui.min.js를 함께 로딩합니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BabylonJS GUI Demo (HUD + 3D UI Panel)</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; }
</style>
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
</head>
<body>
<canvas id="renderCanvas"></canvas>
<script>
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
let scene, camera;
let player;
let health = 100;
let autoMove = false;
// HUD 참조
const hud = {
healthValue: null,
coordValue: null,
healthFill: null,
hintText: null
};
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
function createScene() {
const s = new BABYLON.Scene(engine);
s.clearColor = new BABYLON.Color4(0.06, 0.07, 0.09, 1.0);
camera = new BABYLON.ArcRotateCamera(
"camera",
Math.PI / 2,
Math.PI / 2.6,
18,
new BABYLON.Vector3(0, 1.2, 0),
s
);
camera.attachControl(canvas, true);
camera.wheelDeltaPercentage = 0.01;
const hemi = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), s);
hemi.intensity = 1.0;
// 바닥
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 40, height: 40 }, s);
const gmat = new BABYLON.StandardMaterial("gmat", s);
gmat.diffuseColor = new BABYLON.Color3(0.14, 0.15, 0.17);
gmat.alpha = 0.98;
ground.material = gmat;
ground.isPickable = false;
// 플레이어(간단한 박스)
player = BABYLON.MeshBuilder.CreateBox("player", { size: 1.0 }, s);
player.position.set(0, 0.5, 0);
const pmat = new BABYLON.StandardMaterial("pmat", s);
pmat.diffuseColor = new BABYLON.Color3(0.75, 0.78, 0.90);
player.material = pmat;
// 씬에 약간의 기준물(방향 감각용)
const poleMat = new BABYLON.StandardMaterial("poleMat", s);
poleMat.diffuseColor = new BABYLON.Color3(0.24, 0.26, 0.30);
for (let i = 0; i < 12; i++) {
const pole = BABYLON.MeshBuilder.CreateCylinder("pole" + i, { height: 3.8, diameter: 0.25, tessellation: 24 }, s);
pole.position.set((Math.random() - 0.5) * 30, 1.9, (Math.random() - 0.5) * 30);
pole.material = poleMat;
pole.isPickable = false;
}
// 2D HUD 구성
buildHUD(s);
// 3D UI 구성
build3DPanel(s);
// 입력: WASD 이동(간단)
const keys = { w:false, a:false, s:false, d:false };
window.addEventListener("keydown", (e) => {
const k = e.key.toLowerCase();
if (k === "w") keys.w = true;
if (k === "a") keys.a = true;
if (k === "s") keys.s = true;
if (k === "d") keys.d = true;
if (k === "t") teleportPlayer();
if (k === "h") setHealth(health + 10);
if (k === "j") setHealth(health - 10);
});
window.addEventListener("keyup", (e) => {
const k = e.key.toLowerCase();
if (k === "w") keys.w = false;
if (k === "a") keys.a = false;
if (k === "s") keys.s = false;
if (k === "d") keys.d = false;
});
s.onBeforeRenderObservable.add(() => {
const dt = engine.getDeltaTime() / 1000;
const speed = 6.0;
if (autoMove) {
player.position.x += Math.cos(performance.now() * 0.001) * dt * 2.0;
player.position.z += Math.sin(performance.now() * 0.001) * dt * 2.0;
} else {
let vx = 0, vz = 0;
if (keys.w) vz += 1;
if (keys.s) vz -= 1;
if (keys.d) vx += 1;
if (keys.a) vx -= 1;
const len = Math.hypot(vx, vz);
if (len > 0) {
vx /= len; vz /= len;
player.position.x += vx * speed * dt;
player.position.z += vz * speed * dt;
}
}
// HUD 갱신
updateHUD();
});
return s;
}
// -----------------------------
// 2D HUD (Fullscreen UI)
// -----------------------------
function buildHUD(s) {
const uiTex = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("HUD", true, s);
// 전체 패널(좌상단)
const panel = new BABYLON.GUI.Rectangle("hudPanel");
panel.width = "320px";
panel.height = "150px";
panel.cornerRadius = 12;
panel.thickness = 1;
panel.color = "#ffffff";
panel.background = "#000000";
panel.alpha = 0.45;
panel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
panel.left = "12px";
panel.top = "12px";
uiTex.addControl(panel);
const stack = new BABYLON.GUI.StackPanel("hudStack");
stack.paddingTop = "10px";
stack.paddingLeft = "12px";
stack.paddingRight = "12px";
stack.spacing = 6;
stack.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
panel.addControl(stack);
const title = new BABYLON.GUI.TextBlock("title", "HUD");
title.height = "22px";
title.color = "#ffffff";
title.fontSize = 14;
title.fontWeight = "700";
title.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
stack.addControl(title);
// 체력 라인
const healthRow = new BABYLON.GUI.TextBlock("healthText", "Health: 100");
healthRow.height = "22px";
healthRow.color = "#ffffff";
healthRow.fontSize = 13;
healthRow.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
stack.addControl(healthRow);
hud.healthValue = healthRow;
// 체력 바(배경 + 채움)
const barBg = new BABYLON.GUI.Rectangle("barBg");
barBg.width = "100%";
barBg.height = "14px";
barBg.thickness = 1;
barBg.cornerRadius = 8;
barBg.color = "#ffffff";
barBg.background = "#222222";
barBg.alpha = 0.9;
stack.addControl(barBg);
const barFill = new BABYLON.GUI.Rectangle("barFill");
barFill.width = "100%";
barFill.height = "14px";
barFill.thickness = 0;
barFill.cornerRadius = 8;
barFill.background = "#66ccff";
barFill.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
barBg.addControl(barFill);
hud.healthFill = barFill;
// 좌표 라인
const coord = new BABYLON.GUI.TextBlock("coord", "Pos: (0.00, 0.50, 0.00)");
coord.height = "22px";
coord.color = "#ffffff";
coord.fontSize = 13;
coord.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
stack.addControl(coord);
hud.coordValue = coord;
// 힌트
const hint = new BABYLON.GUI.TextBlock("hint", "WASD 이동 | T 텔레포트 | H 힐 | J 데미지");
hint.height = "36px";
hint.color = "#dddddd";
hint.fontSize = 12;
hint.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
hint.textWrapping = true;
stack.addControl(hint);
hud.hintText = hint;
updateHUD();
}
function updateHUD() {
if (!hud.healthValue) return;
hud.healthValue.text = `Health: ${health}`;
const p = player.position;
hud.coordValue.text = `Pos: (${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)})`;
// 체력 바 폭(%) 갱신
const ratio = clamp(health / 100, 0, 1);
hud.healthFill.width = `${Math.round(ratio * 100)}%`;
// 상태에 따라 색감 변화(제품 느낌을 만드는 작은 포인트)
if (health <= 30) hud.healthFill.background = "#ff6b6b";
else if (health <= 60) hud.healthFill.background = "#ffd166";
else hud.healthFill.background = "#66ccff";
}
function setHealth(v) {
health = clamp(v, 0, 100);
updateHUD();
}
function teleportPlayer() {
player.position.x = (Math.random() - 0.5) * 24;
player.position.z = (Math.random() - 0.5) * 24;
}
// -----------------------------
// 3D UI (World Panel)
// -----------------------------
function build3DPanel(s) {
const manager = new BABYLON.GUI.GUI3DManager(s);
const panel = new BABYLON.GUI.StackPanel3D();
panel.margin = 0.12;
manager.addControl(panel);
// 패널 위치(월드 안의 조작 패널처럼 보이게)
panel.node.position = new BABYLON.Vector3(-6.5, 2.2, 4.0);
// 패널이 항상 카메라를 향하도록(가독성 확보)
s.onBeforeRenderObservable.add(() => {
panel.node.lookAt(camera.position);
});
// 버튼 생성 헬퍼
function addButton(label, onClick) {
const btn = new BABYLON.GUI.HolographicButton("btn_" + label.replace(/\s/g, ""));
btn.text = label;
btn.onPointerUpObservable.add(() => onClick());
panel.addControl(btn);
return btn;
}
// 버튼들
addButton("Damage -10", () => setHealth(health - 10));
addButton("Heal +10", () => setHealth(health + 10));
addButton("Teleport", () => teleportPlayer());
addButton("AutoMove: Toggle", () => { autoMove = !autoMove; });
// 패널 제목 느낌을 위해 “상단에 작은 라벨”을 하나 더 둡니다(버튼을 라벨처럼 활용)
// HolographicButton은 기본적으로 클릭 가능한 형태이므로, 라벨은 클릭해도 부작용이 없도록 빈 동작을 연결합니다.
const label = new BABYLON.GUI.HolographicButton("label");
label.text = "3D UI Panel";
label.onPointerUpObservable.add(() => {});
panel.addControl(label);
// 라벨을 맨 위로 올리기 위해 다시 정렬(간단히 재구성)
// StackPanel3D는 추가된 순서대로 배치되므로, 마지막에 넣은 label을 위로 올리려면 다시 추가하는 방식이 필요할 수 있습니다.
// 여기서는 데모 목적상 label이 아래에 있어도 동작에는 문제 없습니다.
}
// 시작
scene = createScene();
engine.runRenderLoop(() => {
scene.render();
});
window.addEventListener("resize", () => engine.resize());
</script>
</body>
</html>
6-2) (확장) 3D 표면에 2D UI 붙이기: CreateForMesh 패턴
월드에 “모니터/키오스크 화면”처럼 보이는 UI가 필요하다면 GUI3DManager 대신 AdvancedDynamicTexture.CreateForMesh가 직관적입니다. 2D 레이아웃(텍스트/버튼/이미지)을 그대로 쓰되, 결과가 3D 메시에 렌더링됩니다.
// 예: 3D 평면(스크린)에 2D UI를 붙이는 패턴
const screen = BABYLON.MeshBuilder.CreatePlane("screen", { width: 4, height: 2.2 }, scene);
screen.position.set(5, 2, -2);
screen.rotation.y = Math.PI * 0.15;
const adt = BABYLON.GUI.AdvancedDynamicTexture.CreateForMesh(screen, 1024, 512, false);
const rect = new BABYLON.GUI.Rectangle();
rect.thickness = 1;
rect.color = "#ffffff";
rect.background = "#000000";
rect.alpha = 0.55;
adt.addControl(rect);
const text = new BABYLON.GUI.TextBlock();
text.text = "Kiosk UI";
text.color = "#ffffff";
text.fontSize = 44;
rect.addControl(text);
7) 실행 단계 체크리스트
- GUI 스크립트 로딩 확인:
babylon.gui.min.js가 포함되어야 2D/3D GUI가 동작합니다. - HUD는 Fullscreen UI로 시작:
CreateFullscreenUI로 패널(배경)부터 만들고, 그 안에 텍스트/바를 추가합니다. - 3D UI는 작은 패널부터:
GUI3DManager+StackPanel3D+ 버튼 2~3개로 성공 경험을 먼저 만듭니다. - 가독성 확보: 3D 패널은 카메라를 향하도록
lookAt또는 billboard 처리를 적용합니다. - 상태 연결: 버튼 클릭으로 값을 바꾸고, HUD가 그 값을 표시하도록 “데이터 → UI 갱신” 흐름을 고정합니다.
8) 추가로 생각해볼 점
- 제품 느낌은 ‘정렬’에서 나옵니다: 같은 폰트 크기/간격 규칙을 유지하고, 패널을 좌상단 12~16px 여백으로 고정하는 것만으로도 인상이 달라집니다.
- UI 상태(Disabled/Loading/Error)를 준비하세요: 버튼이 비활성일 때 색/알파가 달라지는 규칙을 만들면 앱처럼 보입니다.
- 3D UI는 거리/크기 예산이 필요합니다: 너무 크면 거슬리고, 너무 작으면 안 보입니다. 카메라 기본 거리(ArcRotate radius) 기준으로 패널 크기를 조정하는 습관이 좋습니다.
- 대형 프로젝트에서는 UI도 모듈화: HUD 빌드 함수, 3D 패널 빌드 함수, 상태 스토어(health/score 등)를 분리하면 유지보수가 쉬워집니다.
- 다음 확장 방향: HUD에 미니맵/목표 표시, 3D 패널에 탭/슬라이더/토글, Mesh UI로 키오스크 화면 구성까지 이어가면 “완성형 데모”로 발전합니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글