考虑到一个项目里多个页面可能使用多个模型,因此进行简单的封装
首先创建文件three.js
import * as THREE from 'three/build/three.module'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
export class MyThree {
constructor(container) {
this.container = container
this.scene
this.camera
this.renderer
this.controls
this.init()
* 初始化模型
* @param {Object} container HTMLElemnt
init = () => {
this.scene = new THREE.Scene()
var width = this.container.offsetWidth
var height = this.container.offsetHeight
var k = width / height
* PerspectiveCamera(fov, aspect, near, far)
* Fov – 相机的视锥体的垂直视野角
* Aspect – 相机视锥体的长宽比
* Near – 相机视锥体的近平面
* Far – 相机视锥体的远平面
this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
this.camera.position.set(14, 12, 0.3)
this.camera.rotation.set(-2.1, 1.1, 2.5)
this.renderer = new THREE.WebGLRenderer({
preserveDrawingBuffer: true,
antialias: true,
alpha: true
this.renderer.setSize(width, height)
this.renderer.setClearColor(0xe8e8e8, 0)
this.container.appendChild(this.renderer.domElement)
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.animate()
this.controls.enableDamping = true
this.controls.dampingFactor = 0.1
this.controls.enableZoom = true
this.controls.minDistance = 1
this.controls.target.set(0, 0, 0)
window.onresize = () => {
this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight)
this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
this.camera.updateProjectionMatrix()
render() {
this.renderer.render(this.scene, this.camera)
* 加载模型
* @param {*} path 路径
loadModel = (path) => {
var loader = new GLTFLoader()
loader.load(
path,
(gltf) => {
gltf.scene.traverse(function (child) {
if (child.isMesh) {
gltf.scene.scale.set(0.5, 0.5, 0.5)
this.setModelPosition(gltf.scene)
this.scene.add(gltf.scene)
function (xhr) {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
function (error) {
console.log(error)
console.log('An error happened')
setModelPosition = (object) => {
object.updateMatrixWorld()
const box = new THREE.Box3().setFromObject(object)
const center = box.getCenter(new THREE.Vector3())
object.position.x += object.position.x - center.x
object.position.y += object.position.y - center.y
object.position.z += object.position.z - center.z
getLight() {
const ambient = new THREE.AmbientLight(0xffffff)
this.scene.add(ambient)
animate = () => {
this.controls.update()
this.render()
requestAnimationFrame(this.animate)
然后在vue文件中使用
<template>
<div class="model"></div>
</template>
<script name="mymodel" setup>
import { onMounted, onBeforeUnmount } from 'vue'
import { MyThree } from '../utils/three'
let three
onMounted(() => {
let box = document.querySelector('.model')
three = new MyThree(box)
three.getLight()
three.loadModel('/static/model.gltf')
onBeforeUnmount(() => {
document.querySelector('.model').innerHTML = ''
</script>
到这里,我们已经可以进行模型的加载及控制了,如果只是想看的话,那就已经够了
接下来的功能是与我们的业务相关
向场景中添加热点
热点就是在模型中出现一个标志,始终面向用户,但是会随着模型切换角度而换位置
在three.js中使用的是精灵图THREE.Sprite
constructor
里新增spriteList和eventList
,用来保存当前的精灵图列表以及点击事件
* 添加精灵图
* @param {string} name
* @param {function} cb 回调函数
addSprite = (name, num, position = { x: 2, y: 0.5, z: 0 }, cb) => {
var spriteMap = new THREE.TextureLoader().load('/favicon.ico')
var spriteMaterial = new THREE.SpriteMaterial({
map: spriteMap,
color: 0xffffff,
sizeAttenuation: false
var sprite = new THREE.Sprite(spriteMaterial)
sprite.name = name
sprite.scale.set(0.05
, 0.05, 1)
sprite.position.set(position.x, position.y, position.z)
this.scene.add(sprite)
this.spriteList.push(sprite)
cb && (this.eventList[name] = cb)
* 添加精灵图
* @param {string} name 热点的名字
removeSprite = (name) => {
this.spriteList.some((item) => {
if (item.name === name) {
this.scene.remove(item)
removeAllSprite = () => {
this.spriteList.forEach((item) => {
this.scene.remove(item)
点击热点的事件保存在eventList里,这里的难点在于,如何判定点击到了所选内容,three.js里采用射线法,就是从相机发射一条射线,方向是鼠标位置,如果有相交的对象,那么就是选取的对象
在init函数里添加如下代码
window.onclick = (event) => {
var mouse = { x: 0, y: 0 }
mouse.x = (event.layerX / this.container.offsetWidth) * 2 - 1
mouse.y = -(event.layerY / this.container.offsetHeight) * 2 + 1
var vector = new THREE.Vector3(mouse.x, mouse.y, 0.5).unproject(this.camera)
var raycaster = new THREE.Raycaster(
this.camera.position,
vector.sub(this.camera.position).normalize()
raycaster.camera = this.camera
var intersects = raycaster.intersectObjects(this.scene.children, true)
intersects.forEach((item) => {
this.eventList[item.object.name] && this.eventList[item.object.name]()
相机定位到指定位置,并且要平滑切换
这个功能是用于切换视角使用的,比如定位到某一个热点的位置、定位到所记录的位置
如果想要知道当前的位置,可以直接console.log(three.camera)
,可以获取相机目前的信息
如果想要改变位置,可以直接改变camera的参数,但是想要平滑过渡,就要使用tween.js
首先引入import { TWEEN } from 'three/examples/jsm/libs/tween.module.min'
* 移动视角
moveCamera = (newT = { x: 0, y: 0, z: 0 }, newP = { x: 13, y: 0, z: 0.3 }) => {
let oldP = this.camera.position
let oldT = this.controls.target
let tween = new TWEEN.Tween({
x1: oldP.x,
y1: oldP.y,
z1: oldP.z,
x2: oldT.x,
y2: oldT.y,
z2: oldT.z
tween.to(
x1: newP.x,
y1: newP.y,
z1: newP.z,
x2: newT.x,
y2: newT.y,
z2: newT.z
let that = this
tween.onUpdate((object) => {
that.camera.position.set(object.x1, object.y1, object.z1)
that.controls.target.x = object.x2
that.controls.target.y = object.y2
that.controls.target.z = object.z2
that.controls.update()
tween.onComplete(() => {
this.controls.enabled = true
tween.easing(TWEEN.Easing.Cubic.InOut)
tween.start()
同时animate还要添一句TWEEN.update()
向场景里添加热力图
这里主要是两点,一个是先生成热力图,然后再作为材质添加到场景中
生成热力图可以使用heatmap.js
安装 npm i @rengr/heatmap.js
import h337 from '@rengr/heatmap.js'
export function getHeatmapCanvas(points, x = 500, y = 160) {
var canvasBox = document.createElement('div')
document.body.appendChild(canvasBox)
canvasBox.style.width = x + 'px'
canvasBox.style.height = y + 'px'
canvasBox.style.position = 'absolute'
var heatmapInstance = h337.create({
container: canvasBox,
backgroundColor: 'rgba(255, 255, 255, 0)',
radius: 20,
minOpacity: 0,
maxOpacity: 0.6,
var data
if (points && points.length) {
data = {
max: 40,
min: 0,
data: points,
} else {
let randomPoints = []
var max = 0
var cwidth = x
var cheight = y
var len = 300
while (len--) {
var val = Math.floor(Math.random() * 30 + 20)
max = Math.max(max, val)
var point = {
x: Math.floor(Math.random() * cwidth),
y: Math.floor(Math.random() * cheight),
value: val,
randomPoints.push(point)
data = {
max: 60,
min: 15,
data: randomPoints,
heatmapInstance.setData(data)
let canvas = canvasBox.querySelector('canvas')
document.body.removeChild(canvasBox)
return canvas
这里的方法是使用数据生成热力图,如果没有数据就生成随机数据,其中的参数可以自己调试
有了这个canvas,就可以添加到场景里了
createPlaneByCanvas(name, canvas, position = {}, size = { x: 9, y: 2.6 }, rotation = {}) {
var geometry = new THREE.PlaneGeometry(size.x, size.y)
var texture = new THREE.CanvasTexture(canvas)
var material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide,
transparent: true
texture.needsUpdate = true
const plane = new THREE.Mesh(geometry, material)
plane.material.side = 2
plane.position.x = position.x || 0
plane.position.y = position.y || 0
plane.position.z = position.z || 0
plane.rotation.x = rotation.x || 1.5707963267948966
plane.rotation.y = rotation.y || 0
plane.rotation.z = rotation.z || 0
this.planes[name] = plane
this.scene.add(this.planes[name])
* 根据名称移除热力图
* @param {string} name
removeHeatmap(name) {
this.scene.remove(this.planes[name])
delete this.planes[name]
在热力图处剖切
因为是建筑模型,直接加入热力图,没法看到里面的内容,所以采用剖切
* 增加剖切
addClippingPlanes() {
this.clipHelpers = new THREE.Group()
this.clipHelpers.add(new THREE.AxesHelper(20))
this.globalPlanes = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0)
this.clipHelpers.add(new THREE.PlaneHelper(this.globalPlanes, 20, 0xff0000))
this.clipHelpers.visible = false
this.scene.add(this.clipHelpers)
this.renderer.clippingPlanes = [this.globalPlanes]
this.renderer.localClippingEnabled = true
this.globalPlanes.constant = 0.01
* 设置剖切位置
* @param {number} v
setClippingConstant(v) {
this.globalPlanes.constant = v
* 移除剖切
removeClippingPlanes() {
this.scene.remove(this.clipHelpers)
this.scene.remove(this.globalPlanes)
this.renderer.clippingPlanes = []
导出图片以及gif图
因为three就是渲染在canvas上的,所以直接导出图片很简单
const exportCurrentCanvas = () => {
var a = document.createElement('a')
a.href = three.renderer.domElement.toDataURL('image/png')
a.download = 'image.png'
a.click()
生成gif图使用gif.js,这里与我的实际业务不一样,实际是可以很快就生成n多张图片拼成gif,这里是相当于录像的生成gif,不过原理是一样的
const generateGif = async () => {
var gif = new window.GIF({
workers: 2,
quality: 10
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => {
setTimeout(() => {
console.log(i)
gif.addFrame(three.renderer.domElement, { delay: 200 })
resolve()
}, 200)
gif.on('finished', function (blob) {
window.open(URL.createObjectURL(blob))
gif.render()
最后来张生成的gif的效果图
最后是项目地址: gitee
在线演示: 本人较懒,以后再补