一步步实现react-three-fiber+threejs实现拖动3D对象

目的:有个2个3D盒子,现在想用鼠标拖动每个3d对象盒子,且不和轨道控制冲突

用到的技术:react+react-three-fiber+three.js

一、看three.js文档,分析拖动功能需要的API和参数

three.js文档中可以搜索到 拖动功能 需要用到 拖放控制器(DragControls)

分析:首先拖放控制器(DragControls) 这是一个构造函数,接受3个参数,分别是 objects: 一组可被拖拽的3D Objects。camera: 渲染场景的摄像机。
domElement: 用于事件监听的HTML元素renderer.domElement。

1、在react的three源码里面找DragControls,分析参数和方法、属性

首先,要知道react-three-fiber是针对 Web 和RN上的 threejs 的 React 渲染器。 使用可重用的组件以声明方式构建动态场景图,使 Threejs 的处理变得更加轻松,并使代码库更加整洁。这些组件对状态变化做出反应,具有开箱即用的交互性。
所以在源码找<DragControls/>组件

可以发现在<DragControls/>的路径是"three/examples/jsm/controls/DragControls",在react中结合react-three-fiber库使用该组件需要使用@react-three/fiber的entend功能extend({ DragControls })

其实很多threejs里面的控制功能要在react中以组件方式使用,都需要用到react-three-fiber库里的extend功能,且大多都在three/examples/jsm/controls里面,比如轨道控制器(OrbitControls)和第一人称控制器(FirstPersonControls)

DragControls.PNG

、重要

从源码可看出它和three中一样在构造函数中接受三个参数:3D对象objects、相机camera和dom元素domElement,这三个参数也就是<DragControls/>组件所要接受的参数

二、拿到这三个参数,传给组件<dragControls/> (exend之后使用的时候小写)

1、编写拖动组件包裹拖动对象,拿到object并传递

方式:把要拖动的3D对象组件 用拖动功能组件包裹,并拿到三个参数:3D对象objects、相机camera和dom元素renderer.domElement传给<dragControls/>

// Dragable.jsx
import { DragControls } from "three/examples/jsm/controls/DragControls";
import { extend } from "@react-three/fiber";
extend({ DragControls });
const Dragable = (props) => {
  return (
    <group>
      <dragControls args={[children]} />
      {props.children}
    </group>
  );
};
export default Dragable;

// APP.jsx 包裹要拖动的3D对象
//<BoxTwo /> 组件是个自定义的3维盒子,不知道怎么写可以搜,总之就是mesh网格里面套几何基类组件和材质组件,上面打印children的话会发现有两个mesh
//...
return(//...
         <Dragable>
            <Suspense fallback={...}> <BoxTwo /> </Suspense>
            <Suspense fallback={...}> <BoxTwo position={[1, 1, 1]} /> </Suspense>
          </Dragable>
          //...)

包裹需要拖动的组件之后,children就是3D对象object,这里先把它传给组件<dragControls/>

2、怎么拿到domElement和camera

这三个参数中的domElement是renderer.domElement,(从threejs文档中可以看出),而renderer是渲染器,渲染器在threejs中来源于const renderer = new THREE.WebGLRenderer();
而在react-three-fiber中,渲染器是THREE.WebGLRenderer,被封装在gl这个属性里面,使用时候要在gl里面拿,也就是gl.domElement

因为我们要结合react-three-fiber组件化使用,所以看它的文档,会发现有个hook:useThree,他的作用是访问状态模型,其中包含默认的渲染器(renderer)、场景(scene)、相机(camera)等等。它还会给你提供屏幕和视口坐标中canvas的当前尺寸size。

3、拿到参数

const { camera, gl } = useThree();
传给拖动组件: <dragControls args={[children, camera, gl.domElement]} />

4、添加ref,后续能够访问到它的子对象

这一步是为了后续能够访问到它的子对象,也就是3D object

const groupRef = useRef();
//...
 return( <group ref={groupRef}>
      <dragControls args={[children, camera, gl.domElement]} />
      {props.children}
    </group> )
5、操作3d对象

拖动过程中,我们要设置children状态,并让组件渲染,这就需要用到usestateuseEffect

三、最终代码 (如果未添加轨道控制则over)

import React, { useEffect, useRef, useState } from "react";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import { extend, useThree } from "@react-three/fiber";
extend({ DragControls });

const Dragable = (props) => {
  const { camera, gl } = useThree();
  const [children, setChildren] = useState([]);
  const groupRef = useRef();
  useEffect(() => {
    setChildren(groupRef.current.children);
  }, []);

  return (
    <group ref={groupRef}>
      <dragControls args={[children, camera, gl.domElement]} />
      {props.children}
    </group>
  );
};

export default Dragable;

现在,就可以拖动<Dragable>...</Dragable>包裹的两个3D object了,但前提是没有添加轨道控制组件,否则拖动一个3D对象,整个视图也跟着动了,冲突了

四、补充:bug与轨道控制器冲突解决

方案:添加监听器,当鼠标悬停在某个children时候,禁用轨道控制,然后完成拖动时候,重新启用轨道控制

流程

看threejs文档思考监听怎么用及事件

因为要使用监听,所以我们要用到addEventListener,在three文档中可以看到 **拖放控制器(DragControls)**的Methods下 ”共有方法请参见其基类EventDispatcher。“在共有方法中可以找到addEventListener

也可以见事件
dragstart 当用户开始拖拽3D Objects时触发。
drag 当用户拖拽3D Objects时触发。dragend
当用户开始完成3D Objects时触发。hoveron
当指针移动到一个3D Object或者其某个子级上时触发。hoveroff
当指针移出一个3D Object时触发。

1、看react中DragControls源码看共有方法

在它的源码路径three/examples/jsm/controls/DragControls可以看到

event.PNG
它继承了共有方法EventDispatcher

2、给拖放控制器组件<dragControls/>添加ref

<dragControls ref={controlsRef} args={[children, camera, gl.domElement]} />
原因:要在拖动时候监听鼠标运动,而从上源码可见拖放控制器组件<dragControls/>自带监听addEventListener

3、 怎么让轨道控制器orbitControls禁用和启动

首先,看轨道控制器orbitControls文档,会发现属性那一栏里面:

.enabled : Boolean
当设置为false时,控制器将不会响应用户的操作。默认值为true。
而在orbitControls最终都是都要呈现在场景scene里面的,所以要找到scene,前面说了scene在hook:useThree()里面 const {scene} = useThree();

4、开始监听
 useEffect(() => {
    controlsRef.current.addEventListener("hoveron", (e) => {
      scene.orbitControls.enabled = false;
    });
    controlsRef.current.addEventListener("hoveroff", (e) => {
      scene.orbitControls.enabled = true;
    });
  }, [children]);
5、解决与轨道控制冲突的最终代码
import React, { useEffect, useRef, useState } from "react";
import { DragControls } from "three/examples/jsm/controls/DragControls";
import { extend, useThree } from "@react-three/fiber";
extend({ DragControls });

const Dragable = (props) => {
  //DragControls在three.js文档中可见有三个参数在构造函数中
  //(object:Array,camera:Camera,domElement:HTMLDOMElement),所以这里从useThree里面解构出来
  const { camera, gl, scene } = useThree();
  const [children, setChildren] = useState([]);
  const groupRef = useRef();
  const controlsRef = useRef();

  useEffect(() => {
    setChildren(groupRef.current.children);
  }, []);

  //监听鼠标在3D object上的事件
  // 因为要在拖动每个3D object时候 关闭和启用,所以不能都写在上面的useEffect里面
  // hoveron 当指针移动到一个3D Object或者其某个子级上时触发
  // hoveroff 当指针移出一个3D Object时触发。
  useEffect(() => {
    controlsRef.current.addEventListener("hoveron", (e) => {
      scene.orbitControls.enabled = false;
    });
    controlsRef.current.addEventListener("hoveroff", (e) => {
      scene.orbitControls.enabled = true;
    });
  }, [children]);

  return (
    <group ref={groupRef}>
      <dragControls
        ref={controlsRef}
        args={[children, camera, gl.domElement]}
      />
      {props.children}
    </group>
  );
};

export default Dragable;

至此,可以单独拖动每个3D object了,而且不和轨道控制相互冲突

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值