Frontend Development 13 min read

Three.js API Wrapper and 3D Visualization in a Vue3 Project

This article introduces the fundamentals of Three.js, demonstrates how to encapsulate its core APIs into a Viewer class, and walks through building a Vue3-based 3D visualization project with scene, camera, lighting, model loading, skybox, mouse interaction, and helper utilities.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Three.js API Wrapper and 3D Visualization in a Vue3 Project

Introduction

Three.js is a general-purpose WebGL‑based 3D engine widely used in mini‑games, product showcases, IoT, digital twins, smart city parks, mechanical, architectural, panoramic tours, GIS and many other fields.

The article assumes the reader has a basic understanding of Three.js concepts and APIs; beginners are encouraged to read the official documentation and Chinese tutorials.

It demonstrates how to wrap several common Three.js functionalities into a reusable Viewer class and build a Vue3 project that visualises a 3D scene, including scene, lighting, camera, model loading, skybox, mouse interaction and auxiliary helpers.

Basic Functions – Viewer Class

The Viewer class encapsulates one‑time initialisation of the scene, camera, renderer and lights, separating these concerns from business logic.

1.1 Initialise Scene and Camera

private initScene() {
  this.scene = new Scene();
}
private initCamera() {
  // render camera
  this.camera = new PerspectiveCamera(25, window.innerWidth / window.innerHeight, 1, 2000);
  // set camera position
  this.camera.position.set(4, 2, -3);
  // set camera direction
  this.camera.lookAt(0, 0, 0);
}

1.2 Initialise Camera Controls

private initControl() {
  this.controls = new OrbitControls(this.camera as Camera, this.renderer?.domElement);
  this.controls.enableDamping = false;
  this.controls.screenSpacePanning = false; // disable vertical panning
  this.controls.minDistance = 2;
  this.controls.maxDistance = 1000;
  this.controls.addEventListener('change', () => {
    this.renderer.render(this.scene, this.camera);
  });
}

1.3 Initialise Lights

private initLight() {
  const ambient = new AmbientLight(0xffffff, 0.6);
  this.scene.add(ambient);
  const light = new THREE.DirectionalLight(0xffffff);
  light.position.set(0, 200, 100);
  light.castShadow = true;
  light.shadow.camera.top = 180;
  light.shadow.camera.bottom = -100;
  light.shadow.camera.left = -120;
  light.shadow.camera.right = 400;
  light.shadow.camera.near = 0.1;
  light.shadow.camera.far = 400;
  // improve shadow clarity
  light.shadow.mapSize.set(1024, 1024);
  this.scene.add(light);
}

1.4 Initialise Renderer

private initRenderer() {
  // get canvas DOM
  this.viewerDom = document.getElementById(this.id) as HTMLElement;
  // initialise renderer
  this.renderer = new WebGLRenderer({
    logarithmicDepthBuffer: true,
    antialias: true,
    alpha: true,
    precision: 'mediump',
    premultipliedAlpha: true,
  });
  this.renderer.clearDepth();
  this.renderer.shadowMap.enabled = true;
  this.renderer.outputColorSpace = SRGBColorSpace;
  this.viewerDom.appendChild(this.renderer.domElement);
}

The Viewer also provides helper methods such as addAxis, addStats, etc.

1.5 Mouse Events

Mouse interaction is handled via the mitt event library and Three.js raycasting. The method raycaster.intersectObjects converts screen coordinates to 3D intersections.

/** Register mouse event listeners */
public initRaycaster() {
  this.raycaster = new Raycaster();
  const initRaycasterEvent = (eventName: keyof HTMLElementEventMap): void => {
    const funWrap = throttle((event: any) => {
      this.mouseEvent = event;
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
      // @ts-expect-error
      this.emitter.emit(Events[eventName].raycaster, this.getRaycasterIntersectObjects());
    }, 50);
    this.viewerDom.addEventListener(eventName, funWrap, false);
  };
  // initialise common mouse events
  initRaycasterEvent('click');
  initRaycasterEvent('dblclick');
  initRaycasterEvent('mousemove');
}
public setRaycasterObjects(objList: THREE.Object3D[]): void {
  this.raycasterObjects = objList;
}
private getRaycasterIntersectObjects(): THREE.Intersection[] {
  if (!this.raycasterObjects.length) return [];
  this.raycaster.setFromCamera(this.mouse, this.camera);
  return this.raycaster.intersectObjects(this.raycasterObjects, true);
}

Usage example:

let viewer: Viewer;
viewer = new Viewer('three');
viewer.initRaycaster();
viewer.emitter.on(Event.dblclick.raycaster, (list: THREE.Intersection[]) => {
  onMouseClick(list);
});
viewer.emitter.on(Event.mousemove.raycaster, (list: THREE.Intersection[]) => {
  onMouseMove(list);
});

Model Loader – ModelLoder Class

The ModelLoder class wraps Three.js GLTFLoader and DRACOLoader to load glTF models, optionally using Draco compression. The decoder path must point to a public folder.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import BaseModel from '../BaseModel';
import type Viewer from '../Viewer';

type LoadModelCallbackFn
= (arg: T) => any;

/** Model loader */
export default class ModelLoder {
  protected viewer: Viewer;
  private gltfLoader: GLTFLoader;
  private dracoLoader: DRACOLoader;

  constructor(viewer: Viewer, dracolPath: string = '/draco/') {
    this.viewer = viewer;
    this.gltfLoader = new GLTFLoader();
    this.dracoLoader = new DRACOLoader();
    // provide Draco decoder
    this.dracoLoader.setDecoderPath(dracolPath);
    this.gltfLoader.setDRACOLoader(this.dracoLoader);
  }

  /** Load model into scene */
  public loadModelToScene(url: string, callback: LoadModelCallbackFn
) {
    this.loadModel(url, model => {
      this.viewer.scene.add(model.object);
      callback && callback(model);
    });
  }

  private loadModel(url: string, callback: LoadModelCallbackFn
) {
    this.gltfLoader.load(url, gltf => {
      const baseModel = new BaseModel(gltf, this.viewer);
      callback && callback(baseModel);
    });
  }
}

BaseModel Class

BaseModel wraps a loaded glTF model and provides methods for cloning, playing animations, setting colors, materials, etc.

/** Set model animation */
public startAnima(i = 0) {
  this.animaIndex = i;
  if (!this.mixer) this.mixer = new THREE.AnimationMixer(this.object);
  if (this.gltf.animations.length < 1) return;
  this.mixer.clipAction(this.gltf.animations[i]).play();
  // register animation update
  this.animaObject = {
    fun: this.updateAnima,
    content: this,
  };
  this.viewer.addAnimate(this.animaObject);
}
private updateAnima(e: any) {
  e.mixer.update(e.clock.getDelta());
}

SkyBox – SkyBoxs Class

The SkyBoxs class adds fog effects and a configurable skybox to the scene.

export default class SkyBoxs {
  protected viewer: Viewer;
  constructor(viewer: Viewer) {
    this.viewer = viewer;
  }
  /** Add fog */
  public addFog(color = 0xa0a0a0, near = 500, far = 2000) {
    this.viewer.scene.fog = new THREE.Fog(new THREE.Color(color), near, far);
  }
  /** Remove fog */
  public removeFog() {
    this.viewer.scene.fog = null;
  }
  /** Add default skybox */
  public addSkybox(skyType: keyof typeof Sky = Sky.daytime) {
    const path = `/skybox/${Sky[skyType]}/`;
    const format = '.jpg';
    this.setSkybox(path, format);
  }
  /** Custom skybox */
  private setSkybox(path: string, format = '.jpg') {
    const loaderbox = new THREE.CubeTextureLoader();
    const cubeTexture = loaderbox.load([
      path + 'posx' + format,
      path + 'negx' + format,
      path + 'posy' + format,
      path + 'negy' + format,
      path + 'posz' + format,
      path + 'negz' + format,
    ]);
    cubeTexture.encoding = THREE.sRGBEncoding;
    this.viewer.scene.background = cubeTexture;
  }
}

Outline Helper – BoxHelperWrap

BoxHelperWrap uses Three.js BoxHelper to draw a bounding box around selected objects; it can be toggled visible or hidden.

export default class BoxHelperWrap {
  protected viewer: Viewer;
  public boxHelper: BoxHelper;
  constructor(viewer: Viewer, color?: number) {
    this.viewer = viewer;
    const boxColor = color === undefined ? 0x00ffff : color;
    this.boxHelper = new BoxHelper(new Object3D(), new Color(boxColor));
    this.initBoxHelperWrap();
  }
  private initBoxHelperWrap() {
    this.viewer.scene.add(this.boxHelper);
  }
  public setVisible(visible: boolean): void {
    this.boxHelper.visible = visible;
  }
  public attach(obj: Object3D): void {
    this.boxHelper.setFromObject(obj);
    this.setVisible(true);
  }
  public dispose(): void {
    const parent = this.boxHelper.parent;
    if (parent !== null) {
      parent.remove(this.boxHelper);
    }
    Object.keys(this).forEach(key => {
      // @ts-expect-error
      this[key] = null;
    });
  }
}

Recommended Projects

The implementations are inspired by several open‑source projects, such as:

iot-visualization-examples

threejs-park

MF‑TurbineMonitor (Vue3 hooks version)

Conclusion

Feel free to discuss and share feedback; if the article helped you, consider giving it a like, star, or follow.

JavaScriptFrontend DevelopmentThree.jsWebGLVue33D visualization
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.