Three.js는 웹 브라우저에서 3D 그래픽을 구현하기 위한 강력한 JavaScript 라이브러리로, WebGL을 추상화하여 개발자가 복잡한 그래픽 API를 직접 다루지 않고도 3D 장면을 쉽게 만들 수 있도록 돕습니다. 디지털 트윈 및 다양한 시각화 솔루션에서 3D 모델을 웹에 표시하는 것은 핵심적인 부분이며, Three.js는 이러한 요구사항을 충족시키는 데 이상적인 도구입니다.
다양한 3D 모델링 소프트웨어에서 생성된 모델들을 Three.js 환경으로 가져와 시각화할 수 있습니다. 예를 들어, 3ds Max와 같은 도구로 제작된 모델은 주로 FBX, 3DS, OBJ, STL, DAE와 같은 형식으로 내보내기하여 Three.js에서 로드할 수 있습니다.
Vue.js 프로젝트에 Three.js 설치
Three.js를 Vue.js 애플리케이션에 통합하려면 다음 패키지들을 설치해야 합니다. 터미널에서 아래 명령어를 실행하세요.
npm install --save three
npm install --save three-orbit-controls # 카메라 시점 제어용 플러그인
npm install --save three-obj-mtl-loader # .obj 및 .mtl 파일 로드용 플러그인
npm install --save three-css2drender # 2D HTML 요소를 3D 장면에 오버레이하는 렌더러
기본 3D 객체 표시 예제: 정육면체
다음은 Vue.js 컴포넌트 내에서 Three.js를 사용하여 사용자 정의 가능한 정육면체를 렌더링하는 예제입니다. 슬라이더를 통해 정육면체의 크기와 세그먼트 수를 동적으로 조절할 수 있습니다.
<template>
<div>
<div id="threejs-canvas-cube"></div>
<div class="setting-panel">
<section>
<el-row>
<div v-for="(item, key) in cubeProperties" :key="key">
<div>
<el-col :span="8">
<span class="param-label">{{item.label}}</span>
</el-col>
<el-col :span="13">
<el-slider v-model="item.value" :min="item.min" :max="item.max" :step="item.step"></el-slider>
</el-col>
<el-col :span="3">
<span class="param-label">{{item.value}}</span>
</el-col>
</div>
</div>
</el-row>
</section>
</div>
</div>
</template>
<script>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { SceneUtils } from 'three/examples/jsm/utils/SceneUtils.js';
export default {
name: 'DynamicCubeScene',
data() {
return {
cubeProperties: {
width: { label: '너비', value: 0.5, min: 0, max: 2, step: 0.01 },
height: { label: '높이', value: 0.5, min: 0, max: 2, step: 0.01 },
depth: { label: '깊이', value: 0.5, min: 0, max: 2, step: 0.01 },
widthSegments: { label: '너비 세그먼트', value: 8, min: 1, max: 40, step: 1 },
heightSegments: { label: '높이 세그먼트', value: 8, min: 1, max: 40, step: 1 },
depthSegments: { label: '깊이 세그먼트', value: 8, min: 1, max: 40, step: 1 }
},
mainCamera: null,
sceneGraph: null,
webglRenderer: null,
cubeInstance: null,
orbitController: null
};
},
watch: {
cubeProperties: {
deep: true,
handler() {
if (this.cubeInstance) {
this.rebuildCube();
}
}
}
},
mounted() {
this.init3DScene();
},
methods: {
init3DScene() {
this.setupScene();
this.setupCamera();
this.setupRenderer();
this.createCubeObject();
this.setupLights(); // Optional, adds light to scene
this.setupControls();
this.animateScene();
},
setupScene() {
this.sceneGraph = new THREE.Scene();
},
createCubeObject() {
if (this.cubeInstance) {
this.sceneGraph.remove(this.cubeInstance);
}
const { width, height, depth, widthSegments, heightSegments, depthSegments } = this.cubeProperties;
const cubeGeometry = new THREE.BoxGeometry(
width.value,
height.value,
depth.value,
Math.round(widthSegments.value),
Math.round(heightSegments.value),
Math.round(depthSegments.value)
);
const surfaceMaterial = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
const wireframeMaterial = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x000000 });
this.cubeInstance = SceneUtils.createMultiMaterialObject(cubeGeometry, [surfaceMaterial, wireframeMaterial]);
this.sceneGraph.add(this.cubeInstance);
},
setupLights() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.sceneGraph.add(ambientLight);
},
setupCamera() {
const container = document.getElementById('threejs-canvas-cube');
this.mainCamera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 100);
this.mainCamera.position.z = 2;
},
setupRenderer() {
const container = document.getElementById('threejs-canvas-cube');
this.webglRenderer = new THREE.WebGLRenderer({ antialias: true });
this.webglRenderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(this.webglRenderer.domElement);
},
rebuildCube() {
const currentRotation = this.cubeInstance ? this.cubeInstance.rotation.clone() : new THREE.Euler(0, 0, 0);
this.createCubeObject();
this.cubeInstance.rotation.copy(currentRotation);
},
animateScene() {
requestAnimationFrame(this.animateScene);
if (this.cubeInstance) {
this.cubeInstance.rotation.y += 0.005; // Continuous rotation
}
if (this.orbitController) {
this.orbitController.update(); // Required for controls to work with animation loop
}
this.webglRenderer.render(this.sceneGraph, this.mainCamera);
},
setupControls() {
this.orbitController = new OrbitControls(this.mainCamera, this.webglRenderer.domElement);
this.orbitController.enableDamping = true; // 부드러운 움직임
this.orbitController.dampingFactor = 0.25;
this.orbitController.screenSpacePanning = false;
this.orbitController.maxPolarAngle = Math.PI / 2;
}
}
};
</script>
<style scoped>
#threejs-canvas-cube {
height: 500px;
width: 100%;
background-color: #f0f0f0;
}
.setting-panel {
position: absolute;
left: 10px;
top: 10px;
width: 320px;
padding: 15px;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.param-label {
line-height: 38px;
padding: 0 5px;
font-size: 0.9em;
color: #333;
}
</style>
Three.js는 BoxGeometry 외에도 다양한 형태의 기하학적 객체를 생성할 수 있는 여러 클래스를 제공합니다. 대표적인 예시는 다음과 같습니다:
SphereGeometry(구체)CylinderGeometry(원통)ConeGeometry(원뿔)PlaneGeometry(평면)TorusGeometry(원환체, 도넛 모양)DodecahedronGeometry(12면체)IcosahedronGeometry(20면체)ExtrudeGeometry(2D 모양을 3D로 압출)
구체 객체 생성 예제
위의 정육면체 예제와 유사하게, 구체(Sphere) 객체를 생성하고 그 속성을 조절하는 Vue.js 컴포넌트 예시입니다.
<template>
<div>
<div id="threejs-canvas-sphere"></div>
<div class="setting-panel">
<section>
<el-row>
<div v-for="(item, key) in sphereParameters" :key="key">
<div>
<el-col :span="8">
<span class="param-label">{{item.label}}</span>
</el-col>
<el-col :span="13">
<el-slider v-model="item.value" :min="item.min" :max="item.max" :step="item.step"></el-slider>
</el-col>
<el-col :span="3">
<span class="param-label">{{item.value}}</span>
</el-col>
</div>
</div>
</el-row>
</section>
</div>
</div>
</template>
<script>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { SceneUtils } from 'three/examples/jsm/utils/SceneUtils.js';
export default {
name: 'DynamicSphereScene',
data() {
return {
sphereParameters: {
radius: { label: '반경', value: 8, min: 1, max: 20, step: 0.1 },
widthSegments: { label: '너비 세그먼트', value: 32, min: 3, max: 64, step: 1 },
heightSegments: { label: '높이 세그먼트', value: 32, min: 2, max: 64, step: 1 },
phiStart: { label: 'Phi 시작', value: 0, min: 0, max: Math.PI * 2, step: 0.1 },
phiLength: { label: 'Phi 길이', value: Math.PI * 2, min: 0, max: Math.PI * 2, step: 0.1 },
thetaStart: { label: 'Theta 시작', value: 0, min: 0, max: Math.PI, step: 0.1 },
thetaLength: { label: 'Theta 길이', value: Math.PI, min: 0, max: Math.PI, step: 0.1 }
},
currentSphere: null,
sceneEnv: null,
viewCamera: null,
webglViewport: null,
orbitControls: null
};
},
watch: {
sphereParameters: {
deep: true,
handler() {
if (this.currentSphere) {
this.updateSphereGeometry();
}
}
}
},
mounted() {
this.initializeSphereScene();
},
methods: {
initializeSphereScene() {
this.createSceneEnvironment();
this.setupLighting();
this.configureCamera();
this.createWebGLViewport();
this.createSphereMesh();
this.enableOrbitControls();
this.startSphereAnimation();
},
createSceneEnvironment() {
this.sceneEnv = new THREE.Scene();
this.webglViewport.setClearColor(0x282c34, 1); // Dark background
},
createSphereMesh() {
if (this.currentSphere) {
this.sceneEnv.remove(this.currentSphere);
}
const { radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength } = this.sphereParameters;
const sphereGeometry = new THREE.SphereGeometry(
radius.value,
widthSegments.value,
heightSegments.value,
phiStart.value,
phiLength.value,
thetaStart.value,
thetaLength.value
);
const surfaceMaterial = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
const wireframeMaterial = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x000000 });
this.currentSphere = SceneUtils.createMultiMaterialObject(sphereGeometry, [surfaceMaterial, wireframeMaterial]);
this.sceneEnv.add(this.currentSphere);
},
setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
this.sceneEnv.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(-30, 40, 20);
directionalLight.castShadow = true;
this.sceneEnv.add(directionalLight);
},
configureCamera() {
const canvasElement = document.getElementById('threejs-canvas-sphere');
const aspect = canvasElement.clientWidth / canvasElement.clientHeight;
this.viewCamera = new THREE.PerspectiveCamera(50, aspect, 0.1, 2000);
this.viewCamera.position.set(-50, 40, 30);
this.viewCamera.lookAt(new THREE.Vector3(0, 0, 0));
this.sceneEnv.add(this.viewCamera);
},
createWebGLViewport() {
const canvasElement = document.getElementById('threejs-canvas-sphere');
this.webglViewport = new THREE.WebGLRenderer({ antialias: true, alpha: true });
this.webglViewport.setSize(canvasElement.clientWidth, canvasElement.clientHeight);
this.webglViewport.shadowMap.enabled = true;
this.webglViewport.shadowMap.type = THREE.PCFSoftShadowMap;
this.webglViewport.setClearColor(0x3f3f3f, 1);
canvasElement.appendChild(this.webglViewport.domElement);
},
updateSphereGeometry() {
const currentRotation = this.currentSphere ? this.currentSphere.rotation.clone() : new THREE.Euler(0, 0, 0);
this.createSphereMesh();
this.currentSphere.rotation.copy(currentRotation);
},
startSphereAnimation() {
requestAnimationFrame(this.startSphereAnimation);
if (this.currentSphere) {
this.currentSphere.rotation.y += 0.005;
}
if (this.orbitControls) {
this.orbitControls.update();
}
this.webglViewport.render(this.sceneEnv, this.viewCamera);
},
enableOrbitControls() {
this.orbitControls = new OrbitControls(this.viewCamera, this.webglViewport.domElement);
this.orbitControls.enableDamping = true;
this.orbitControls.dampingFactor = 0.25;
}
}
};
</script>
<style scoped>
#threejs-canvas-sphere {
position: absolute;
width: 100%;
height: 100%;
background-color: #f0f0f0;
}
.setting-panel {
position: absolute;
left: 10px;
top: 10px;
width: 320px;
padding: 15px;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.param-label {
line-height: 38px;
padding: 0 5px;
font-size: 0.9em;
color: #333;
}
</style>