在更改 window 的 ThreeJS VueJS 应用程序中获得正确的鼠标悬停交互

Get correct mouseover interaction in a ThreeJS VueJS app changing the window

我为我所做的感到非常自豪:我有一个包含 4 个形状的菜单。当您悬停一个形状时,它会改变颜色、增长并将其他形状推到顶部,同时旋转速度会变慢。

我阅读了 ThreeJS 文档并遵循了 Whosebug 成员的建议。

我在鼠标交互和 window 调整大小方面遇到了困难:当我第一次打开浏览器时,鼠标悬停似乎并没有在鼠标悬停时准确调用。 当我调整 window 的大小时,它显然搞砸了。 如果有人知道我做错了什么,请提前致谢:)

这是我的组件:

<template>
  <v-container>
    <div @click="onClick" @mousemove="onMouseMove" id="menu3D" style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;"></div>
    <v-row class="text-center">

      <v-col
        class="mb-5"
        cols="12"
      >
        <h2 class="headline font-weight-bold mb-3">
          Accueil
        </h2>

        <v-row justify="center">

          <p>
            THIS IS ONLY A TEST
          </p>
        </v-row>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>

  import * as Three from 'three'

  export default {
    name: 'Home',
    mounted() {
      this.init();
    },
    methods: {
      init: function() {
       this.createScene();
       this.createCamera();
       this.userData.formes.forEach(x=>this.createShape(x))
       this.addSpotlight(16777215/*'#fdffab'*/);
       this.addAmbientLight();
       this.animate();
       window.addEventListener('resize', this.onResize())
      },
      onResize: function() {
       let container = document.getElementById('menu3D');
       this.renderer.setSize(container.clientWidth, container.clientHeight);
       this.camera.aspect = container.clientWidth / container.clientHeight;
       this.camera.updateProjectionMatrix();
      },
      createScene: function() {

       this.renderer = new Three.WebGLRenderer({
         antialias: true,
         alpha: true
       });
       let container = document.getElementById('menu3D');
       this.renderer.setSize(container.clientWidth, container.clientHeight);
       this.renderer.setPixelRatio(window.devicePixelRatio);
       this.renderer.setClearColor(0xffffff,0);
       container.appendChild(this.renderer.domElement);
      },

      createCamera: function() {
        //let container = document.getElementById('container');
       this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);
       this.camera.position.set(0, 5, 20);
       this.camera.zoom = 1;
      },

      createShape: function(shape) {

       let material = new Three.MeshStandardMaterial({
        "color": '#0000ff'/*16777215*/,
        "roughness": 1,
        "metalness": 0.5,
        "emissive": 0,
        "depthFunc": 3,
        "depthTest": true,
        "depthWrite": true,
        "stencilWrite": false,
        "stencilWriteMask": 255,
        "stencilFunc": 519,
        "stencilRef": 0,
        "stencilFuncMask": 255,
        "stencilFail": 7680,
        "stencilZFail": 7680,
        "stencilZPass": 7680
       })
       switch (shape.nom) {
         case "Box": {
           this.geometry = new Three.BoxBufferGeometry(1.8,1.8,1.8)
           break;
         }
         case "Sphere": {
           this.geometry = new Three.SphereBufferGeometry(1,8,6,0,6.283185,0, 3.141593)
           break;
         }
         case "Dodecahedron": {
           this.geometry = new Three.DodecahedronBufferGeometry(1.2,0)
           break;
         }
         case "Icosahedron": {
           this.geometry = new Three.IcosahedronBufferGeometry(1.5,0)
           break;
         }
       }
       this.mesh = new Three.Mesh(this.geometry, material)
       this.mesh.name = shape.nom
       this.mesh.userData = shape.userData
       this.mesh.receiveShadow = true
       this.mesh.castShadow = true
       this.mesh.position.set(0, shape.userData.position.y, 0)
       this.scene.add(this.mesh)
      },

      addSpotlight: function(color) {
       const light = new Three.SpotLight(color, 2, 1000)
       light.position.set(0, 0, 30)
       this.scene.add(light)
      },

      addAmbientLight: function() {
       const light = new Three.AmbientLight('#fff', 0.5)
       this.scene.add(light)
      },

      verifForme: function(e) {
        let t = this
        let elt = t.scene.getObjectByName(e);
            t.intersects = t.raycaster.intersectObject(elt);
            if (t.intersects.length !== 0) {
                // if it's not in the array, we put it at the beginning
                if (t.userData.souris.indexOf(e)<0) {
                    t.userData.souris.unshift(e);
                    console.log(t.userData.souris[0] + " survolé!");
                }
                if (t.userData.souris[0] == e) {
                    let obj = t.intersects[0].object;
                    obj.material.color.set('#'+elt.userData.couleurs[1]);
                    obj.scale.set(obj.scale.x<1.4?obj.scale.x+t.VITESSE_ZOOM:obj.scale.x,obj.scale.y<1.4?obj.scale.y+t.VITESSE_ZOOM:obj.scale.y,obj.scale.z<1.4?obj.scale.z+t.VITESSE_ZOOM:obj.scale.z);
                    obj.rotation.y += t.VITESSE_ROTATION/t.RALENTISSEMENT
                    t.replacer(obj,obj.userData.position.y+obj.userData.decalage)

                }
                else {
                    t.retrecir(e,elt);
                }
        }
            else {
                if (t.userData.souris.indexOf(e)>=0) {
                    t.userData.souris = t.userData.souris.filter(forme => forme != e);
                }
                t.retrecir(e,elt);
            }
      },

      onClick: function ( event ) {
        event.preventDefault();
            if (this.userData.souris.length >0 ) { console.log(this.userData.souris[0] + " clicked!"); }
        else {
          console.log("click outside!")
        }
        },

      onMouseMove: function(event){
        let container = document.getElementById('menu3D');
        this.mouse.x = ( event.clientX / container.clientWidth ) * 2 - 1;
        this.mouse.y = - ( event.clientY / container.clientHeight ) * 2 + 1;
        //console.log(JSON.stringify(this.mouse))
      },

      replacer: function(e,py) {
            // next line to prevent shaking
            if (Math.abs(e.position.y - py) < 0.05) { return true }
            let rhesus = 10*this.VITESSE_ZOOM
        if (this.userData.souris[0] != e.name) { rhesus *= 3 }
        //console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
            if (e.position.y > py) { rhesus = -1 }
            e.position.set(0,Math.trunc(10*e.position.y+rhesus)/10,0)
        },

      retrecir: function (n,e) {
            // checking if the clicked element is on top
            let dec = 0
        let elt = this
            if ((elt.userData.souris.length > 0) && (elt.userData.formes.map(x=>x.nom).indexOf(n)<elt.userData.formes.map(x=>x.nom).indexOf(elt.userData.souris[0]))) {
                dec = Math.trunc(10*e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage*2.1)/10;
            }
            e.material.color.set('#'+e.userData.couleurs[0]);
            e.rotation.y += elt.VITESSE_ROTATION
            e.scale.set(e.scale.x>1?e.scale.x-elt.VITESSE_ZOOM:e.scale.x,e.scale.y>1?e.scale.y-elt.VITESSE_ZOOM:e.scale.y,e.scale.z>1?e.scale.z-elt.VITESSE_ZOOM:e.scale.z);
            let newY = e.userData.position.y+dec
            if (e.position.y != newY) {
                elt.replacer(e,newY)
            }
        },

      animate: function() {
         let elt = this
         requestAnimationFrame(this.animate);
         this.raycaster.setFromCamera(this.mouse, this.camera);
         this.userData.formes.map(x=>x.nom).forEach(x=>elt.verifForme(x))
         if (this.userData.souris.length >0 ) { document.body.style.cursor = "pointer"; }
         else { document.body.style.cursor = "default"; }
         this.camera.updateProjectionMatrix();
         this.renderer.render(this.scene, this.camera);
      }
    },
    data: () => ({
      scene: new Three.Scene(),
      camera: null,
      renderer: Three.WebGLRenderer,
      mesh: new Three.Mesh,
      factor:0,
      mouse : new Three.Vector2(1, 1),
      raycaster : new Three.Raycaster(),
      intersects : [],
      VITESSE_ROTATION: 0.05,
      VITESSE_ZOOM: 0.1,
      RALENTISSEMENT: 3,
      userData: {
        "souris": [],
        "formes": [
          {
            "nom": "Box",
            "userData": {
              "position": {
                "x": 0,
                "y": 7.8,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "095256"
              ],
              "decalage": 0.5
            }
          },
          {
            "nom": "Icosahedron",
            "userData": {
              "position": {
                "x": 0,
                "y": 5.5,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "087F8C"
              ],
              "decalage": 0.5
            }
          },
          {
            "nom": "Dodecahedron",
            "userData": {
              "position": {
                "x": 0,
                "y": 3.1,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "5AAA95"
              ],
              "decalage": 0.4
            }
          },
          {
            "nom": "Sphere",
            "userData": {
              "position": {
                "x": 0,
                "y": 1,
                "z": 0
              },
              "couleurs": [
                "aaaaaa",
                "86A873"
              ],
              "decalage": 0.2
            }
          }
        ]
      }
    }),
  }
</script>

恐怕问题出在你相机的创建上:

this.camera = new Three.PerspectiveCamera(50, 1.686275 /*container.clientWidth/container.clientHeight*/, 0.01, 1000);

为什么您使用神奇的宽高比 1.686275 而不是像调整大小时那样使用实际的 width/height 比率?这会在调整大小之前和之后为您提供不同的行为。

乍一看这是我最好的猜测,尽管我认为您的应用中还有其他硬编码“幻数”实例需要根据屏幕的宽度和高度重新计算。我不可能阅读您发布的 300 行代码。您应该考虑隔离问题以创建一个 minimal working example and add it to your question via a code snippet 以便我们可以看到您的代码在运行。

我使用您提供的代码调查了这个问题并在本地修复了它,所以我希望它也适用于您。问题如下:

  1. 在调整大小事件的附件中有一个很难发现的拼写错误:您需要使用 window.addEventListener('resize', this.onResize); 而不是 window.addEventListener('resize', this.onResize()) 删除 () 因为您不想调用附件时的函数,您希望每次触发事件时调用它。
  2. 由于第一个问题,调整大小函数没有按预期调用,我想这就是导致您在相机实例化中使用硬编码值 (1.686275) 而不是推荐公式的原因container.clientWidth / container.clientHeight 所以你需要把它改回

createCamera: function () {
    let container = document.getElementById('menu3D');
    this.camera = new Three.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.01, 1000);
    ...

  1. 另外,由于VueJS的要求,3D容器div不在HTMLbody的根级,在onMouseMove()中需要消耗偏移量坐标而不是客户端坐标如下:

onMouseMove: function (event) {
  let container = document.getElementById('menu3D');
  this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
  this.mouse.y = - (event.offsetY / container.clientHeight) * 2 + 1;
  ...