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