第三章


虽然现在基本的模型都创建出来了,但是我们3D会议室的主要目的是可以自己摆放会议室中的物品,自己设定布局,所以我们需要能够对会议室中的模型进行位置拖动与角度旋转的操作来达到这一高度自由的效果。

拖动

DragControls

说到拖动效果,就必须讲到Three.js的拖动对象DragControls,它为Three.js提供了便捷的拖动交互
直接上代码讲解

initDragControls(objects, info) {
      let that = this;
      // 初始化拖拽控件
      this.dragControls = new DragControls(
        objects,
        this.camera,
        this.renderer.domElement
      );
      this.dragControls.transformGroup = true;
      // 开始拖拽
      this.dragControls.addEventListener("dragstart", this.setDragstart);
      this.dragControls.addEventListener("drag", function (e) {
        if (e.object.position.y < -60) {
           e.object.position.y = -60;
         }
      });
      // 拖拽结束
      this.dragControls.addEventListener("dragend", this.setDragend);
    },
	// 具名拖拽开始函数
    setDragstart() {
      this.rotateBtnShow = false;
      this.editWellObject = false;
      this.controls.enabled = false;
    },
    // 具名拖拽结束函数
    setDragend() {
      this.controls.enabled = true;
    },

首先不要忘了导入,import { DragControls } from "three/examples/jsm/controls/DragControls";
注册了DragControls对象后,为我们提供了5个事件
dragstart:当用户开始拖拽3D Objects时触发;
drag:当用户拖拽3D Objects时触发;
dragend:当用户开始完成3D Objects时触发;
hoveron:当指针移动到一个3D Object或者其某个子级上时触发;
hoveroff:当指针移出一个3D Object时触发。Dran
要注意在我们注册事件的时候,能用具名函数就用具名函数,因为这样做可以再页面销毁时移除注册的事件,优化性能。
此次项目中,为了达到重力效果,也就是桌子、椅子之类的物品,在拖动时不能离开地面,所以我在drag中加入了对当前操作模型y轴的限制,以此来达到以下效果:
在这里插入图片描述
对于多个3D模型,我们需要注册多个DragControls对象,因为每个拖拽对象会对应相应的模型,如果仅仅注册一个DragControls对象,将会造成无论移动哪一个模型,都只是第一个模型在移动的景象

旋转

因为Three.js并未直接提供让3D模型进行旋转的对象,所以我们为了达到旋转的效果,一共分为两部

获取鼠标选中的3D模型

想要选中一个物体主要是依靠Raycaster这个对象,它主要用于鼠标拾取,也就是计算鼠标在3维空间移动时触碰了哪些物体。原理就是在鼠标所指的位置会发射一条射线,一条垂直鼠标,垂直电脑屏幕的射线,Three.js将获取到该射线依次经过的物体。
其实对于这个射线的理解,我们也可以换一种方式,比如浏览器开发者模式,如下图
在这里插入图片描述
点击该按钮后可以查看页面的元素,鼠标经过的元素会在Elements中展示,下面上代码

getIntersects(event) {
      event.preventDefault();

      let raycaster = new THREE.Raycaster();
      let mouse = new THREE.Vector2();

      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

      //通过鼠标点击的位置(二维坐标)和当前相机的矩阵计算出射线位置
      raycaster.setFromCamera(mouse, this.camera);

      // 获取与射线相交的对象数组,其中的元素按照距离排序,越近的越靠前
      let moudle = this.scene.children.filter((item) => {
        if (item.type === "Group") {
          return item;
        }
      });
      let intersects = raycaster.intersectObjects(moudle, true);
      if (intersects.length > 0) {
        let object = intersects[0].object;
        this.getGroup(object);
        return this.selectGroup;
      }
      return {};
    },
    // 获取组
    getGroup(intersects) {
      let selectGroup = intersects.parent;
      if (selectGroup.type === "Group") {
        return (this.selectGroup = selectGroup);
      } else {
        selectGroup = selectGroup.parent;
        this.getGroup(selectGroup);
      }
    },

首先注册Raycaster对象,获取鼠标的坐标,根据坐标和当前相机(因为相机的视角也是一个很重要的因素)计算出射线的位置,根据raycaster.intersectObjects()方法获取与射线相交的3D对象,然后根据3D对象获取其所在的3D组,因为外部加载的3D模型一般都会是以组的形式出现,射线可能只是触碰到该组的某一个对象。

// 鼠标单击触发的事件
    mouseClick(event) {
      let intersects = this.getIntersects(event);
      if (intersects !== {} && intersects.type === "Group") {
        this.selectObject = intersects;
        if (this.selectObject.name === "可旋转") {
          this.rotateBtnShow = true;
          this.addRotateClick(this.selectObject);
        } else {
          this.editWellObject = true;
        }
      } else {
        this.rotateBtnShow = false;
        this.selectObject = null;
        this.editWellObject = false;
      }
    },

很显然我们需要注册鼠标点击事件去获取因触发射线与3D对象相交后得到的组,鼠标注册事件放在mounted中,这里我就不具体提了,在此也要注意一点,因为鼠标是全局注册,所以当点击空白出时由于获取不到相关对象,可能会导致报错,并且我们也并不是所有的3D模型都需要旋转效果,比如墙面和地板本就是不支持的,所以我们需要添加一个筛选条件,也就是intersects.type === "Group"当获取到的结果是一个组时,才去触发旋转的效果;(代码中未注册的方法后面代码中会有)

添加旋转按钮以及旋转方法

获取到被选中的物体之后,我们就要为其赋予可旋转的方法,为了区分当前物体是否可旋转以及突出当前选中的物体,首先创建一个旋转的按钮出来。

createRotateBtn() {
     let halWidth = window.innerWidth / 2;
     let halHeight = window.innerHeight / 2;
     let vector = this.selectObject.position.clone().project(this.camera);
     this.$refs.rotateBtn.style.left = `${
       halWidth + vector.x * halWidth - 40
     }px`;
     this.$refs.rotateBtn.style.top = `${
       -vector.y * halHeight + halHeight - this.selectObject.position.y - 230
     }px`;
   },

其实旋转按钮的位置主要就是靠Css定位来决定,所以我们要通过被选中的3D模型的坐标来计算旋转按钮的lefttop

   addRotateClick() {
     if (this.addRotate) {
       this.addRotate = false;
       let that = this;
       let newX = Number(this.$refs.rotateBtn.style.left.replace("px", "")); // 数据格式转
       document.addEventListener("dragenter ", function (e) {
         e.preventDefault();
       });
       document.addEventListener("dragover", function (e) {
         e.preventDefault();
       });
       // 旋转事件
       this.$refs.rotateBtn.addEventListener(
         "drag",
         function (event) {
           if (event.x < newX) {
             that.selectObject.rotateY(Math.PI / 120);
           } else if (event.x > newX) {
             that.selectObject.rotateY(Math.PI / -120);
           }
           newX = event.x;
         },
         true
       );
     }

如果旋转事件一直注册,不仅消耗内存,而且还会使旋转的速度越来越快, 为了避免旋转事件被一直注册,所以设定addRotate来作为一个开关,对于旋转事件我主要赋予旋转按钮上,通过旋转按钮的拖动来影响3D模型的旋转角度。
以当前按钮的坐标为基准,如果向左拖动按钮,对应的3D模型就进行逆时针旋转,旋转的幅度可以自己设置,用selectObject.rotateY(Math.PI / 120)方法达到被选中物体的旋转,同理,往右拖动按钮,对应的3D模型就进行顺时针旋转。
上效果:在这里插入图片描述

小贴士

在拖动的时候,可能因为鼠标移到了旋转按钮而停止拖动,所以在之前拖动的代码中,在开始拖动的回调函数里对旋转按钮进行的隐藏。
因为Three.js会对鼠标发出的射线经过的所有物体进行统计,所以在拖动物体时,摄像机角度可能也会被拖动,所以也要在开始拖动的回调函数中禁用轨道控制器,不让摄像机被旋转。

Logo

前往低代码交流专区

更多推荐