在vue 项目中用 three.js 帮我实现点击任意模型弹框,弹框显示在模型正上方,弹框可以自定义宽高样式,弹框内显示自定义的文字内容

2023-04-12by@2044352134-hSHz9&@ChatGPT.Model: gpt-3.5
2044352134-hSHz9

在vue 项目中用 three.js 帮我实现点击任意模型弹框,弹框显示在模型正上方,弹框可以自定义宽高样式,弹框内显示自定义的文字内容

Avatar
Avatar

首先在vue项目中引入three.js库:

import * as THREE from 'three'

然后在vue组件中定义一个three.js场景,加载模型并添加点击事件:

<template>
  <div ref="container" style="width: 100%; height: 100%;"></div>
</template>

<script>
import * as THREE from 'three'

export default {
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      raycaster: new THREE.Raycaster(),
      mouse: new THREE.Vector2(),
      intersected: null,
      popup: null,
      popupText: 'Hello world!'
    }
  },
  mounted() {
    this.initScene()
    this.initCamera()
    this.initRenderer()
    this.loadModel()
    this.renderScene()
    window.addEventListener('resize', this.onWindowResize, false)
    this.$refs.container.addEventListener('mousemove', this.onMouseMove, false)
    this.$refs.container.addEventListener('click', this.onClick, false)
  },
  methods: {
    initScene() {
      this.scene = new THREE.Scene()
    },
    initCamera() {
      this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000)
      this.camera.position.set(0, 0, 5)
    },
    initRenderer() {
      this.renderer = new THREE.WebGLRenderer({ antialias: true })
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      this.renderer.setPixelRatio(window.devicePixelRatio)
      this.$refs.container.appendChild(this.renderer.domElement)
    },
    loadModel() {
      const loader = new THREE.GLTFLoader()
      loader.load('/path/to/model.gltf', gltf => {
        const model = gltf.scene
        model.position.set(0, 0, 0)
        model.scale.set(1, 1, 1)
        this.scene.add(model)
        this.intersected = model
      })
    },
    renderScene() {
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.renderScene)
    },
    onWindowResize() {
      this.camera.aspect = window.innerWidth / window.innerHeight
      this.camera.updateProjectionMatrix()
      this.renderer.setSize(window.innerWidth, window.innerHeight)
    },
    onMouseMove(event) {
      event.preventDefault()
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
    },
    onClick(event) {
      event.preventDefault()
      if (this.intersected) {
        const intersects = this.raycaster.intersectObject(this.intersected, true)
        if (intersects.length > 0) {
          this.showPopup()
        }
      }
    },
    showPopup() {
      if (!this.popup) {
        this.popup = document.createElement('div')
        this.popup.style.position = 'absolute'
        this.popup.style.top = '0'
        this.popup.style.left = '0'
        this.popup.style.width = '200px'
        this.popup.style.height = '200px'
        this.popup.style.background = 'white'
        this.popup.style.border = '1px solid black'
        this.popup.style.padding = '10px'
        this.popup.style.textAlign = 'center'
        this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
        this.popup.innerHTML = this.popupText
        this.$refs.container.appendChild(this.popup)
      }
      const position = this.intersected.getWorldPosition(new THREE.Vector3())
      const screenPosition = position.clone().project(this.camera)
      const x = (screenPosition.x + 1) / 2 * window.innerWidth
      const y = -(screenPosition.y - 1) / 2 * window.innerHeight
      this.popup.style.top = `${y}px`
      this.popup.style.left = `${x}px`
    }
  }
}
</script>

在这个例子中,我们创建了一个three.js场景,并加载了一个模型。我们在模型上添加了鼠标点击事件,当用户点击模型时,我们会显示一个弹框。弹框的内容可以自定义,弹框的位置会显示在模型的正上方。

我们在vue组件的data中定义了一些变量,包括场景、相机、渲染器、射线、鼠标位置、被选中的模型、弹框和弹框内容。我们还定义了一些方法来初始化场景、相机

Avatar

、渲染器、加载模型、渲染场景、处理窗口大小改变事件、处理鼠标移动事件、处理鼠标点击事件和显示弹框。

在加载模型时,我们使用了GLTFLoader来加载模型文件。一旦模型加载完成,我们将其添加到场景中,并将其保存为被选中的模型。

当用户在模型上点击鼠标时,我们使用射线检测来检测是否有模型被选中。如果有,我们将显示一个弹框。

在显示弹框时,我们首先检查是否已经存在一个弹框。如果没有,我们创建一个新的div元素作为弹框,并设置一些样式属性。我们还设置弹框的内容为我们在data中定义的popupText变量的值。

然后,我们需要确定弹框的位置。我们使用被选中的模型的世界位置来计算屏幕上的位置。我们将这个位置映射到屏幕坐标系,并使用这个坐标来设置弹框的位置。最后,我们将弹框添加到vue组件的DOM元素中。

需要注意的是,在实际应用中,我们需要通过CSS来控制弹框的样式,而不是直接设置style属性。我们在这个例子中直接设置style属性只是为了演示方便。

最后,我们需要在vue组件销毁时清理所有事件和元素:

beforeDestroy() {
  window.removeEventListener('resize', this.onWindowResize)
  this.$refs.container.removeEventListener('mousemove', this.onMouseMove)
  this.$refs.container.removeEventListener('click', this.onClick)
  if (this.popup) {
    this.$refs.container.removeChild(this.popup)
  }
},

这样,我们就成功地在vue项目中使用three.js实现了点击任意模型弹框的效果。

Avatar

如果我们需要在弹框中显示不同的内容,我们可以通过传递参数来实现。例如,我们可以在showPopup方法中传递一个参数来设置弹框内容:

showPopup(content) {
  if (!this.popup) {
    this.popup = document.createElement('div')
    this.popup.style.position = 'absolute'
    this.popup.style.top = '0'
    this.popup.style.left = '0'
    this.popup.style.width = '200px'
    this.popup.style.height = '200px'
    this.popup.style.background = 'white'
    this.popup.style.border = '1px solid black'
    this.popup.style.padding = '10px'
    this.popup.style.textAlign = 'center'
    this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
    this.$refs.container.appendChild(this.popup)
  }
  this.popup.innerHTML = content
  // ...
}

然后,在点击事件中调用showPopup方法时,我们传递需要显示的内容,例如:

onClick(event) {
  event.preventDefault()
  if (this.intersected) {
    const intersects = this.raycaster.intersectObject(this.intersected, true)
    if (intersects.length > 0) {
      this.showPopup('This is some custom content')
    }
  }
},

这样,我们就可以在弹框中显示不同的内容了。

最后,需要注意的是,在实际应用中,我们需要根据需求来设置弹框的样式和布局,以及处理弹框中的交互事件。在这个例子中,我们只是简单地创建了一个div元素作为弹框,但在实际应用中,我们可能需要使用更复杂的UI组件库来实现弹框。

Avatar

另外,如果我们需要在弹框中显示更复杂的内容,例如图表或表格等,我们可以考虑使用第三方库来实现。例如,我们可以使用Chart.js来绘制图表,将图表渲染到canvas元素中,然后将canvas元素添加到弹框中:

import Chart from 'chart.js/auto'

showPopup(chartData) {
  if (!this.popup) {
    this.popup = document.createElement('div')
    this.popup.style.position = 'absolute'
    this.popup.style.top = '0'
    this.popup.style.left = '0'
    this.popup.style.width = '400px'
    this.popup.style.height = '400px'
    this.popup.style.background = 'white'
    this.popup.style.border = '1px solid black'
    this.popup.style.padding = '10px'
    this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
    this.$refs.container.appendChild(this.popup)
  }
  const canvas = document.createElement('canvas')
  canvas.width = 380
  canvas.height = 380
  const ctx = canvas.getContext('2d')
  new Chart(ctx, {
    type: 'bar',
    data: chartData,
    options: {}
  })
  this.popup.innerHTML = ''
  this.popup.appendChild(canvas)
  // ...
}

在这个例子中,我们使用Chart.js来创建一个柱状图,并将图表渲染到canvas元素中。然后,我们将canvas元素添加到弹框中。这样,我们就可以在弹框中显示复杂的图表了。

需要注意的是,在使用第三方库时,我们需要在vue组件中正确安装和卸载库,以避免内存泄漏和其他问题。具体的实现方法会因库而异,需要查阅库的文档来了解。

2044352134-hSHz9

你写的代码有错误,怎么点击了没反应,而且保存

Avatar
Avatar

非常抱歉,我的代码中有些错误。以下是经过修正的代码:

模板代码:

<template>
  <div ref="container" style="width: 100%; height: 100%;"></div>
</template>

脚本代码:

import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export default {
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      raycaster: new THREE.Raycaster(),
      mouse: new THREE.Vector2(),
      intersected: null,
      popup: null,
      popupText: 'Hello world!'
    }
  },
  mounted() {
    this.initScene()
    this.initCamera()
    this.initRenderer()
    this.loadModel()
    this.renderScene()
    window.addEventListener('resize', this.onWindowResize, false)
    this.$refs.container.addEventListener('mousemove', this.onMouseMove, false)
    this.$refs.container.addEventListener('click', this.onClick, false)
  },
  methods: {
    initScene() {
      this.scene = new THREE.Scene()
    },
    initCamera() {
      this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000)
      this.camera.position.set(0, 0, 5)
    },
    initRenderer() {
      this.renderer = new THREE.WebGLRenderer({ antialias: true })
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      this.renderer.setPixelRatio(window.devicePixelRatio)
      this.$refs.container.appendChild(this.renderer.domElement)
    },
    loadModel() {
      const loader = new GLTFLoader()
      loader.load('/path/to/model.gltf', gltf => {
        const model = gltf.scene
        model.position.set(0, 0, 0)
        model.scale.set(1, 1, 1)
        this.scene.add(model)
        this.intersected = model
      })
    },
    renderScene() {
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.renderScene)
    },
    onWindowResize() {
      this.camera.aspect = window.innerWidth / window.innerHeight
      this.camera.updateProjectionMatrix()
      this.renderer.setSize(window.innerWidth, window.innerHeight)
    },
    onMouseMove(event) {
      event.preventDefault()
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
    },
    onClick(event) {
      event.preventDefault()
      if (this.intersected) {
        const intersects = this.raycaster.intersectObject(this.intersected, true)
        if (intersects.length > 0) {
          this.showPopup()
        }
      }
    },
    showPopup() {
      if (!this.popup) {
        this.popup = document.createElement('div')
        this.popup.style.position = 'absolute'
        this.popup.style.top = '0'
        this.popup.style.left = '0'
        this.popup.style.width = '200px'
        this.popup.style.height = '200px'
        this.popup.style.background = 'white'
        this.popup.style.border = '1px solid black'
        this.popup.style.padding = '10px'
        this.popup.style.textAlign = 'center'
        this.popup.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'
        this.$refs.container.appendChild(this.popup)
      }
      const position = this.intersected.getWorldPosition(new THREE.Vector3())
      const screenPosition = position.clone().project(this.camera)
      const x = (screenPosition.x + 1) / 2 * window.innerWidth
      const y = -(screenPosition.y - 1) / 2 * window.innerHeight
      this.popup.style.top = `${y}px`
      this.popup.style.left = `${x}px`
      this.popup.innerHTML = this.popupText
    }
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.onWindowResize)
    this.$refs.container.removeEventListener('mousemove', this.onMouseMove)
    this.$refs.container.removeEventListener('click', this.onClick)
    if (this.popup) {
      this.$refs.container.removeChild(this.popup)
    }
  }
}

在这个修正后的代码中,我将GLTFLoader导入语句改为了:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

同时,在loadModel方法中,我将GLTFLoader的实例化方式改为了:

const loader = new GLTFLoader()

这是因为在

2044352134-hSHz9

为什么运行起来画面是黑色的,也没报错

Avatar
Avatar

A server error has occurred

INTERNAL_SERVER_ERROR