Vue.js 환경에서 Three.js 3D 객체 시각화

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>

태그: Vue.js Three.js WebGL 3D Rendering OrbitControls

8월 31일 11:27에 게시됨