ThreeJs之选中模型中的物体及物体沿轨迹移动

本案例用到的技能:
1.加载模型
2.相机移动
3.鼠标选中模型
4.在场景中加入新的物体
5.物体沿路径移动
5.第一视角巡视
7.canvas画布自适应-页面窗口改变时模型不变形

首先新建react项目,引入Three,新建一个加载模型的class方法类,用于处理模型相关的操作。
主要思想是将图形组件与前端页面分离,降低图形组件与前端业务逻辑的耦合度。在图形组件中将方法暴露出来,前端页面来调用即可。

图形组件的class方法

初始化及加载模型:

  constructor(canvas, modelPath = "./models/") {
    this.camera = new PerspectiveCamera(
      45,
      canvas.width / canvas.height,
      0.1,
      1000
    );
    // this.camera.position.set(0, 10, 15);
    this.camera.position.set(
      2.4595794148094408,
      13.499328043352282,
      0.8119053487039843
    );

    this.camera.lookAt(0, 0, 0);
    this.normalCamera = this.camera;

    this.renderer = new WebGLRenderer({ canvas });
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.modelPath = modelPath;
    this.maps.set(
      "cabinet-hover.jpg",
      new TextureLoader().load(`${modelPath}cabinet-hover.jpg`)
    );
    this.creatBox();
    this.addRoutes();
    this.addLight();
  }
loadGLTF(modelName = "") {
    gltfLoader.load(this.modelPath + modelName, ({ scene: { children } }) => {
      children.forEach((obj) => {
        const { color, map, name } = obj.material;

        this.changeMat(obj, map, color);
        if (name.includes("cabinet")) {
          this.cabinets.push(obj);
        }
      });
      this.scene.add(...children);
    });
  }
// 修改Mesh 对象的材质 
 changeMat(obj, map, color) {
    if (map) {
      obj.material = new MeshBasicMaterial({
        map: this.crtTexture(map.name),
      });
    } else {
      obj.material = new MeshBasicMaterial({ color });
    }
    if (obj.name === "cabinet-010") {
      obj.cameraPosition = {
        x: 1.3604732850786612,
        y: 2.6105915306590584,
        z: 6.199682831211108,
      };
      obj.controlsPosition = {
        x: 2.415939275692724,
        y: -0.5427880982932958,
        z: 0.38954958313860316,
      };
    }
    if (obj.name === "cabinet-020") {
      obj.cameraPosition = {
        x: 0.6089158377757249,
        y: 2.3543090823589896,
        z: 3.302157733332054,
      };
      obj.controlsPosition = {
        x: 2.725367213430147,
        y: 0.38903559697724144,
        z: -0.07168470117887349,
      };
    }
    if (obj.name === "cabinet-030") {
      obj.cameraPosition = {
        x: -0.4005966957451239,
        y: 2.6068066780517345,
        z: 1.0418153228082048,
      };
      obj.controlsPosition = {
        x: 2.064112146693374,
        y: 1.4519662140510958,
        z: -1.0561189115306107,
      };
    }
  }

  crtTexture(imgName) {
    let curTexture = this.maps.get(imgName);
    if (!curTexture) {
      curTexture = new TextureLoader().load(this.modelPath + imgName);
      curTexture.flipY = false;
      curTexture.wrapS = 1000;
      curTexture.wrapT = 1000;
      this.maps.set(imgName, curTexture);
    }
    return curTexture;
  }

渲染场景

  // 渲染
  render() {
    this.renderer.render(this.scene, this.camera);
  }
  // 连续渲染
  animate(time) {
    requestAnimationFrame((time) => {
      this.animate(time);
    });
    if (this.resizeRendererToDisplaySize(this.renderer)) {
      const { clientWidth, clientHeight } = this.renderer.domElement;
      this.camera.aspect = clientWidth / clientHeight; // camera.aspect是 相机视口的宽高比
      this.camera.updateProjectionMatrix(); // 更新透视投影矩阵。
    }
    this.renderer.render(this.scene, this.camera);
  }

canvas画布自适应窗口变化的方法:

  // 将渲染尺寸设置为其显示的尺寸,返回画布像素尺寸是否等于其显示(css)尺寸的布尔值
  resizeRendererToDisplaySize(renderer) {
    // width 元素宽度  clientWidth 可见内容的宽度
    const { width, height, clientWidth, clientHeight } = renderer.domElement;
    const needResize = width !== clientWidth || height !== clientHeight;
    // true 不等  false 相等
    if (needResize) {
      // 是重置渲染尺寸的方法  // setSize() 方法中的bool 参数很重要,会用于判断是否设置canvas 画布的css 尺寸。
      // 设置了canvas画布的宽高
      this.renderer.setSize(clientWidth, clientHeight, false);
    }
    return needResize;
  }

鼠标选中模型的方法

 //   选择机柜
// x,y为传入的鼠标点击的坐标  moveOrClick参数判断鼠标是点击还是覆盖物体
  selectCabinet(x, y, moveOrClick) {
    const { cabinets, renderer, camera, maps, curCabinet } = this;
    const { width, height } = renderer.domElement;

    // 鼠标的canvas坐标转裁剪坐标
    pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
    // 基于鼠标点和相机设置射线投射器  // 通过摄像机和鼠标位置更新射线
    raycaster.setFromCamera(pointer, camera);
    // 选择机柜 // 计算物体和射线的焦点
    const intersect = raycaster.intersectObjects(cabinets)[0];
    let intersectObj = intersect ? intersect.object : null;
    // 若之前已有机柜被选择,且不等于当前所选择的机柜,取消已选机柜的高亮
    if (curCabinet && curCabinet !== intersectObj) {
      const material = curCabinet.material;
      material.setValues({
        map: maps.get("cabinet.jpg"),
      });
    }
    /* 
    若当前所选对象不为空:
      触发鼠标在机柜上移动的事件。
      若当前所选对象不等于上一次所选对象:
        更新curCabinet。
        将模型高亮。
        触发鼠标划入机柜事件。
    否则:
      置空curCabinet。
      触发鼠标划出机柜事件。
    */
    if (intersectObj) {
      //   console.log("intersectObj", intersectObj);
      if (moveOrClick === "click") {
        this.onClickCabinet(intersectObj);
      }

      this.onMouseMoveCabinet(x, y);
      if (intersectObj !== curCabinet) {
        this.curCabinet = intersectObj;
        const material = intersectObj.material;
        material.setValues({
          map: maps.get("cabinet-hover.jpg"),
        });
        this.onMouseOverCabinet(intersectObj);
      }
    } else if (curCabinet) {
      this.curCabinet = null;
      this.onMouseOutCabinet();
    }
  }

// 鼠标点击时相机移动,即改变相机位置和中心点位置,以显示点击物体的最佳视角

  // 点击某个机柜
  onClickCabinet = (intersectObj) => {
    if (intersectObj.cameraPosition) {
      this.camera.position.set(
        intersectObj.cameraPosition.x,
        intersectObj.cameraPosition.y,
        intersectObj.cameraPosition.z
      );
      this.controls.target.set(
        intersectObj.controlsPosition.x,
        intersectObj.controlsPosition.y,
        intersectObj.controlsPosition.z
      );
      this.controls.update();
    }
  };

在场景中加入一个新的立方体,并在立方体上加入相机,后面物体沿轨迹移动时调用物体身上的相机,实现以物体的视角展示模型

// 创建一个立方体,并且创建了立方体的相机,放到坐标系中
  creatBox() {
    window.camera = this.camera;
    window.controls = this.controls;
    // 坐标辅助线
    let axis = new THREE.AxesHelper(10);
    this.scene.add(axis);
    const geometry = new BoxGeometry(0.5, 0.5, 0.5); //几何对象
    // 一种用于具有镜面高光的光泽表面的材质
    const material = new MeshPhongMaterial({ color: 0x00ff00 }); // 材质
    const cube = new Mesh(geometry, material);
    cube.position.set(8, 0.25, -0.45);
    this.scene.add(cube);
    this.cube = cube;

    const boxCamera = new THREE.PerspectiveCamera(75, 2, 0.1, 1000);
    boxCamera.position.y = 1;
    boxCamera.position.z = 0.5;
    boxCamera.position.x = 1;
    boxCamera.rotation.y = Math.PI * 0.5;
    cube.add(boxCamera);
    this.boxCamera = boxCamera;
  }

在场景中加入路径,物体沿着此路径移动

 //   在模型中建立路径
  addRoutes = () => {
    const curve = new THREE.SplineCurve([
      new THREE.Vector2(8, -0.5),
      new THREE.Vector2(-7, -0.5),
      new THREE.Vector2(-7, -3),
      new THREE.Vector2(8, -3),
      new THREE.Vector2(8, -0.5),
    ]);

    const points = curve.getPoints(50);
    const geometry = new THREE.BufferGeometry().setFromPoints(points);
    const material = new THREE.LineBasicMaterial({ color: 0x888888 });
    const splineObject = new THREE.Line(geometry, material);
    splineObject.rotation.x = Math.PI * 0.5;
    splineObject.position.y = 0.05;
    this.scene.add(splineObject);
    this.curveRef.current = curve;
  };

// 立方体沿路径移动和停止的方法,调用时执行

cubeMovePosition = new THREE.Vector2();
  stopMove = null;
  // 盒子沿路径移动
  moveBox(time) {
    if (time) {
      const curve = this.curveRef.current;
      const tankTime = (time / 600) * 0.05;

      this.cubeMovePosition = curve.getPointAt(tankTime % 1);
      this.cube.position.set(
        this.cubeMovePosition.x,
        0,
        this.cubeMovePosition.y
      );
    }

    this.stopMove = requestAnimationFrame((time) => {
      this.moveBox(time);
    });
  }
  // 停止移动
  stopMoveBox() {
    cancelAnimationFrame(this.stopMove);
  }

// 调用立方体上面的相机,展示第一视角

 // 启用巡视相机位
 xunshiCamera(isStart) {
   if (isStart) {
     this.camera = this.boxCamera;
   } else {
     this.camera = this.normalCamera;
   }
 }

前端页面调用

页面中展示模型的canvas、信息div,操作按钮

   <div
        className="App"
        onMouseMove={this.mouseMove}
        onClick={this.clickCabinet}
      >
        <canvas id="canvas" ref={(ele) => (canvas = ele)}></canvas>

        <div id="plane" style={{ left, top, display }}>
          <p>机柜名称:{name}</p>
          <p>机柜温度:{temperature}°</p>
          <p>
            使用情况:{count}/{capacity}
          </p>
        </div>

        <div className="xunshi" onClick={this.startPatrol.bind(this)}>
          {patrolState}巡视
        </div>

      </div>

实例化模型加载方法

 componentDidMount() {
    if (!canvas) {
      return;
    }
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    room = new MachineRoom(canvas);
    room.modelPath = "./models/";
    room.loadGLTF("machineRoom.gltf");
    room.animate();
    room.onMouseOverCabinet = (cabinet) => {
      //显示信息面板
      this.setState({
        planeDisplay: "block",
      });
    };
    room.onMouseMoveCabinet = (left, top) => {
      //移动信息面板
      this.setState({
        planePos: { left, top },
      });
    };
    room.onMouseOutCabinet = () => {
      //显示信息面板
      this.setState({
        planeDisplay: "none",
      });
    };
  }

// 鼠标移动和点击时将鼠标的位置传过去,在图形组件中转裁剪坐标,以判断是否选中物体

  mouseMove({ clientX, clientY }) {
    room.selectCabinet(clientX, clientY);
  }
  clickCabinet({ clientX, clientY }) {
    room.selectCabinet(clientX, clientY, "click");
  }

// 点击巡视按钮时调用立方体上的相机,并使立方体沿轨迹移动

  // 点击巡视
  startPatrol() {
    const nowState = this.state.patrolState;
    this.setState({ patrolState: nowState === "开始" ? "暂停" : "开始" });
    const open = nowState !== "暂停";
    room.xunshiCamera(open);
    if (open) {
      room.moveBox();
    } else {
      room.stopMoveBox();
    }
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352