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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.