Use three.js to load the model and add a heat map and generate a GIF
preface
We usually use packaged platforms to load models in our own company, but for simple projects there is no need to use those platforms, you can simply make one yourself
What is a Threejs
- Threejs Chinese website
- The document
- github
How do I use Threejs
The installation
npm: npm install three --save-dev
yarn: yarn add three --save-dev
Initialize the
Given that multiple pages in a project may use multiple models, simple encapsulation is performed
First create the file three.js
import * as THREE from 'three/build/three.module'
// This model is in GLTF format, so use GLTFLoader, other available loader can see the official website
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()
}
/** * Initialize the model *@param {Object} container HTMLElemnt
*/
init = () = > {
this.scene = new THREE.Scene()
var width = this.container.offsetWidth // Window width
var height = this.container.offsetHeight // Window height
var k = width / height // Window width ratio
/** * PerspectiveCamera(fov, aspect, near, Far) * Fov -- Vertical field Angle of the camera's visual cone * Aspect -- Aspect ratio of the camera's visual cone * Near -- Near plane of the camera's visual cone * FAR -- Far plane of the camera's visual cone */
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.// Anti-aliasing, the product does not say the model is not good, let me add
antialias: true.alpha: true
})
this.renderer.setSize(width, height)
this.renderer.setClearColor(0xe8e8e8.0)
this.container.appendChild(this.renderer.domElement)
/** OrbitControls are used for dragging and rotating mouse */
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.animate()
// Whether there is inertia in damping or rotation when the animation is recycled
this.controls.enableDamping = true
// The dynamic damping coefficient is the mouse drag rotation sensitivity
this.controls.dampingFactor = 0.1
this.controls.enableZoom = true
this.controls.minDistance = 1 // Limit the zoom
// controls.maxDistance = 30
this.controls.target.set(0.0.0) // Rotate the center point
window.onresize = () = > {
// Reset the renderer output canvas size
this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight)
// In full screen mode: Set the aspect ratio of the viewing range to the aspect ratio of the window
this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
// the renderer executes the render method to read the camera object's projectionMatrix property projectionMatrix
// But the projection matrix will not be calculated by the camera attributes every frame rendered (saving computing resources)
// If some of the camera's attributes change, the updateProjectionMatrix () method is used to update the camera's projection matrix
this.camera.updateProjectionMatrix()
}
}
// Render function
render() {
this.renderer.render(this.scene, this.camera) // Perform render operations
}
/** * Load model *@param {*} Path the path * /
loadModel = (path) = > {
var loader = new GLTFLoader()
loader.load(
path,
(gltf) = > {
gltf.scene.traverse(function (child) {
if (child.isMesh) {
// child.geometry.center() // center here
// If the model is very dark after loading, it can be turned on to add the missing material
// 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) // Automatic center, can be used if the project needs
this.scene.add(gltf.scene)
},
function (xhr) {
// Listen for model loading progress
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')},function (error) {
// A callback for loading errors
console.log(error)
console.log('An error happened')})}// Auto center
setModelPosition = (object) = > {
object.updateMatrixWorld()
// Get min and Max from the bounding box
const box = new THREE.Box3().setFromObject(object)
// Return the width, height, and depth of the bounding box
// const boxSize = box.getSize()
// console.log(box)
// Return to the center of the bounding 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
}
// Add light source, there are many kinds of light source, you can try various light source and parameters
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)
}
/** * animation */
animate = () = > {
// Update the controller
this.controls.update()
this.render()
requestAnimationFrame(this.animate)
}
}
Copy the code
It is then used in a Vue file
<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>
Copy the code
At this point, we are ready to load and control the model, if we just want to see it, that is enough
The next feature is relevant to our business
Add a hotspot to a scenario
A hot spot is a flag that appears in the model, always facing the user, but changing position as the model switches angles. Saves the current Sprite chart list and click events
/** * Add Sprite map *@param {string} name
* @param {function} Cb callback function */
addSprite = (name, num, position = { x: 2, y: 0.5, z: 0 }, cb) = > {
// Use the image to make the material, I don't have the image here, use the Favicon to make the material directly
var spriteMap = new THREE.TextureLoader().load('/favicon.ico')
// Generate Sprite texture
var spriteMaterial = new THREE.SpriteMaterial({
map: spriteMap,
color: 0xffffff.sizeAttenuation: false
})
var sprite = new THREE.Sprite(spriteMaterial)
// Save the name for recording, then remove and click events
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)
// If there is a callback function, it will be executed when clicked
cb && (this.eventList[name] = cb)
}
/** * Add Sprite map *@param {string} Name Indicates the hotspot name */
removeSprite = (name) = > {
this.spriteList.some((item) = > {
if (item.name === name) {
this.scene.remove(item)
}
})
}
/** Remove all hotspots */
removeAllSprite = () = > {
this.spriteList.forEach((item) = > {
this.scene.remove(item)
})
}
Copy the code
The hotspot click event is stored in the eventList. The difficulty here is how to determine if the selected item is clicked. In Three. js, the ray method is used, which means that a ray is emitted from the camera in the direction of the mouse, and if there are intersecting objects, it is the selected objects
window.onclick = (event) = > {
// Convert the mouse click position on the left side of the screen to the standard coordinates in 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)
// Emits a ray from the camera through the standard coordinate position, that is, the selected object
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]()
})
}
Copy the code
The camera is positioned in the specified position, and the switch should be smooth
This function is used to switch the perspective, such as locating the location of a hot spot, locating the recorded location
If you want to know the current location, you can directly console. Log (three. Camera) to get the current information of the camera
If you want to change the position, can directly change the parameters of the camera, but want a smooth transition, will use the tween. Js firstly import {tween} from ‘three/examples/JMS/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()
}
Copy the code
TWEEN. Update ()
Add a heat map to the scene
Npmi@rengr/Heatmap.js can be installed using 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 (0102256,0.2)'
radius: 20./ / [0, + up)
minOpacity: 0.maxOpacity: 0.6,})// Build some random data points and replace them with your business data
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,
}
}
// Since data is a set of data, we directly setData
heatmapInstance.setData(data)
let canvas = canvasBox.querySelector('canvas')
document.body.removeChild(canvasBox)
return canvas
}
Copy the code
The idea here is to use data to generate a thermal map, and if you don’t have data you generate random data, and the parameters in that can be debugged by themselves and you can add to the scene with this canvas
// Add an object with canvas material
createPlaneByCanvas(name, canvas, position = {}, size = { x: 9, y: 2.6 }, rotation = {}) {
var geometry = new THREE.PlaneGeometry(size.x, size.y) // Generate a plane
var texture = new THREE.CanvasTexture(canvas) // Introduce materials
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 // Double-sided material
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])
}
/** * Removes heat map * by name@param {string} name* /
removeHeatmap(name) {
this.scene.remove(this.planes[name])
delete this.planes[name]
}
Copy the code
Cut at the heat map
Because it is an architectural model, it is impossible to see the content inside by directly adding the thermal diagram, so cutting is adopted
/** ** */
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) // Like a thermograph, a plane is generated
this.clipHelpers.add(new THREE.PlaneHelper(this.globalPlanes, 20.0xff0000))
this.clipHelpers.visible = false
this.scene.add(this.clipHelpers)
// // Create a section
// console.log(renderer, globalPlanes)
this.renderer.clippingPlanes = [this.globalPlanes] // display the profile
this.renderer.localClippingEnabled = true
this.globalPlanes.constant = 0.01 // Set the position slightly above the map, otherwise the map cannot be seen
}
/** * Set the cutting position *@param {number} v* /
setClippingConstant(v) {
this.globalPlanes.constant = v
}
/** ** remove the cut */
removeClippingPlanes() {
this.scene.remove(this.clipHelpers)
this.scene.remove(this.globalPlanes)
this.renderer.clippingPlanes = []
}
Copy the code
Export images and GIFs
Since three is rendered on the canvas, it’s easy to export the image directly
const exportCurrentCanvas = () = > {
var a = document.createElement('a')
a.href = three.renderer.domElement.toDataURL('image/png')
a.download = 'image.png'
a.click()
}
Copy the code
Generate GIF map using GIF. Js, here is different from my actual business, in fact, can quickly generate N more pictures into GIF, here is equivalent to the video generated GIF, but the principle is the same
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()
}
Copy the code
Finally, a rendering of the generated GIF
At the end
- Finally, the project address: Gitee
- Online demo:
I am lazy, I will make it up later