使用Three.js加载模型并且添加热力图及生成gif

您所在的位置:网站首页 threejs数字孪生热力图 使用Three.js加载模型并且添加热力图及生成gif

使用Three.js加载模型并且添加热力图及生成gif

2023-05-22 03:33| 来源: 网络整理| 查看: 265

使用Three.js加载模型并且添加热力图及生成gif 前言

我们在自己的公司要加载模型通常使用封装好的平台,但是遇到简单的项目没必要使用那些平台,可以自己简单做一个

什么是Threejs Threejs中文网 文档 github 如何使用Threejs 安装

npm: npm install three --save-dev yarn: yarn add three --save-dev

初始化

考虑到一个项目里多个页面可能使用多个模型,因此进行简单的封装

首先创建文件three.js

import * as THREE from 'three/build/three.module' // 该模型是gltf格式的,所以使用GLTFLoader,,其他可使用loader可以看官网 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) /** 轨道控制器(OrbitControls)用于鼠标的拖拽旋转等操作 */ 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 // 限制缩放 // controls.maxDistance = 30 this.controls.target.set(0, 0, 0) // 旋转中心点 window.onresize = () => { // 重置渲染器输出画布canvas尺寸 this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight) // 全屏情况下:设置观察范围长宽比aspect为窗口宽高比 this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源) // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵 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) { // child.geometry.center() // center here // 如果加载模型后发现模型非常暗,可以开启,会将丢失的材质加上 // child.material.emissive = child.material.color // child.material.emissiveMap = child.material.map } }) gltf.scene.scale.set(0.5, 0.5, 0.5) // scale here 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() // 获得包围盒得min和max const box = new THREE.Box3().setFromObject(object) // 返回包围盒的宽度,高度,和深度 // const boxSize = box.getSize() // console.log(box) // 返回包围盒的中心点 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) // const ambient = new THREE.AmbientLight(0xcccccc, 3.5) // const ambient = new THREE.HemisphereLight(0xffffff, 0x000000, 1.5) // ambient.position.set(30, 30, 0) this.scene.add(ambient) } /** * 动画 */ animate = () => { // 更新控制器 this.controls.update() this.render() requestAnimationFrame(this.animate) } } 复制代码

然后在vue文件中使用

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 = '' }) 复制代码

到这里,我们已经可以进行模型的加载及控制了,如果只是想看的话,那就已经够了 model

接下来的功能是与我们的业务相关

向场景中添加热点

热点就是在模型中出现一个标志,始终面向用户,但是会随着模型切换角度而换位置 在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) => { // 使用图片做材质,我这里没图片,直接使用favicon做材质了 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) => { // 将鼠标点击位置的屏幕左边转换成three.js中的标准坐标 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]() }) } 复制代码

spirite

相机定位到指定位置,并且要平滑切换

这个功能是用于切换视角使用的,比如定位到某一个热点的位置、定位到所记录的位置

如果想要知道当前的位置,可以直接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 // return console.log(this.controls) 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 }, 2000 ) 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()

move

向场景里添加热力图

这里主要是两点,一个是先生成热力图,然后再作为材质添加到场景中 生成热力图可以使用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)', // '#121212' 'rgba(0,102,256,0.2)' radius: 20, // [0,+∞) 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, } } // 因为data是一组数据,所以直接setData heatmapInstance.setData(data) let canvas = canvasBox.querySelector('canvas') document.body.removeChild(canvasBox) return canvas } 复制代码

这里的方法是使用数据生成热力图,如果没有数据就生成随机数据,其中的参数可以自己调试 有了这个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 // color: '#fff' }) 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) // //创建一个剖切面 // console.log(renderer, globalPlanes) 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 = [] } 复制代码

heatmap

导出图片以及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 < 60; i++) { // setCamera() 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的效果图 gif

结尾 最后是项目地址: gitee 在线演示: 本人较懒,以后再补


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3