目的:有个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)
、重要
从源码可看出它和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状态,并让组件渲染,这就需要用到usestate
和useEffect
三、最终代码 (如果未添加轨道控制则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可以看到
它继承了共有方法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了,而且不和轨道控制相互冲突