はじめに
皆様こんにちは。
AMDlab Webエンジニアの塚田です。
react-three/fiberとreact-three/dreiを使い始めて3ヶ月程経ちました。dreiの用意してくれている機能は数多くあり、便利なものが揃っています。
そこで今回は最近実装された DragControls という機能についてご紹介したいと思います。
DragControlsはメッシュをつかんで動かす機能を簡単に作成することができます。
それでは説明に入ります。
説明
react、typescript、react-three/fiber、dreiについては事前に準備してください。(私はNext.jsを使用しています。)
完成したプロジェクトのフォルダ構成は以下のようになります。
1 2 3 4 5 |
app └─ dreiDragControl ├─ draggableObjects.tsx ├─ meshInfo.ts └─ page.tsx |
page.tsx内のCanvas内のデザイン等については以下を参考にさせていただきました。(勉強になりました、ありがとうございます。)
【React Three Fiber】DraggableなMeshの実装
https://qiita.com/nemutas/items/c49728da8641ee28fd2e
DragControls
Drei ドキュメント
https://drei.docs.pmnd.rs/gizmos/drag-controls
ドキュメントの紹介に以下のコードがあります。
1 2 3 |
<DragControls> <mesh /> </DragControls> |
本当にこれだけで実装できます。以下実装例です。
実装例
page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// App routerの場合は忘れずに! "use client"; import React from "react"; import { Canvas } from "@react-three/fiber"; import { DragControls } from "@react-three/drei"; import { DoubleSide } from "three"; const DreiDragControl = () => { return ( <div style={{ width: "100vw", height: "100vh" }}> <Canvas camera={{ position: [0, 10, 8], fov: 50, aspect: window.innerWidth / window.innerHeight, near: 0.1, far: 2000, }} dpr={window.devicePixelRatio} shadows > <color attach="background" args={["#1e1e1e"]} /> <DragControls> <mesh position={[0, 0, 0.5]}> <planeGeometry args={[1, 1]} /> <meshStandardMaterial color={"red"} side={DoubleSide} /> </mesh> </DragControls> <axesHelper /> <gridHelper position={[0, 0.01, 0]} args={[10, 10, "red", "black"]} /> <ambientLight intensity={2.0} /> <pointLight position={[10, 10, 10]} /> </Canvas> </div> ); }; export default DreiDragControl; |
これだけだと本当に紹介だけになってしまうので、以下の二つの機能を追加したいと思います。
- 重なっているメッシュを選択した時は手前のもののみ動かせるようにする。(通常時、下記のように重なっていると重複したまま動かせてしまう)
2.メッシュを掴んで動かしている間はOrbitControls※1の回転機能をロックしてメッシュを動かしやすくする。
※1.dreiの機能。sceneの回転やパン、ズームを操作可能にする。
実装コード
draggableObjects.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
import React, { useEffect, useRef, useState } from "react"; import { useThree } from "@react-three/fiber"; import { DragControls } from "@react-three/drei"; import { DoubleSide, Group, Vector2 } from "three"; import { getSampleMeshInfos } from "./meshInfo"; type Props = { setEnableRotate: React.Dispatch<React.SetStateAction<boolean>>; }; export const DraggableObjects: React.FC<Props> = ({ setEnableRotate }) => { const groupRef = useRef<Group>(null); const { camera, raycaster } = useThree(); const [mode, setMode] = useState(`0`); const originalMeshInfos = getSampleMeshInfos(); const ghostMeshInfos = getSampleMeshInfos(); const handleMouseMove = (event: MouseEvent) => { // 手前優先の処理 const current = new Vector2(); current.x = (event.clientX / window.innerWidth) * 2 - 1; current.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(current, camera); const intersects = raycaster.intersectObjects(groupRef!.current!.children); if (intersects[0] != null) { setMode(intersects[0].object.name); } else { setMode(`0`); } }; useEffect(() => { window.addEventListener("mousemove", handleMouseMove); return () => { window.removeEventListener("mousemove", handleMouseMove); }; }, []); const handleOnDragStart = () => { setEnableRotate(false); }; const handleOnDragEnd = () => { setEnableRotate(true); }; return ( <> <group ref={groupRef}> {originalMeshInfos.map((meshInfo, index) => ( <DragControls key={`drag-control-${index + 1}`} onDragStart={handleOnDragStart} onDragEnd={handleOnDragEnd} autoTransform={mode === `${index + 1}`} > <mesh key={`mesh-${index + 1}`} name={`${index + 1}`} position={meshInfo.position} rotation={meshInfo.rotation} > <planeGeometry args={[1, 1]} /> <meshStandardMaterial color={meshInfo.color} side={DoubleSide} /> </mesh> </DragControls> ))} </group> {/** 基準のメッシュをゴースト表示 */} <group> {ghostMeshInfos.map((meshInfo, index) => ( <mesh key={`ghost-drag-control-${index + 1}`} position={meshInfo.position} rotation={meshInfo.rotation} > <planeGeometry args={[1, 1]} /> <meshStandardMaterial color={meshInfo.color} side={DoubleSide} opacity={0.3} transparent={true} /> </mesh> ))} </group> </> ); }; |
このファイルのコードが今回のメイン部分になります。
1.重なっているメッシュを選択した時は手前のもののみ動かせるようにする。
について
handleMouseMove内でメッシュの手前のものを区別する判定を実装しています。
また、DragControlsの prop にて autoTransform を手前のもののみtrueにすることで
掴んで動かせるものを手前のものに限定しています。
2.メッシュを掴んで動かしている間はOrbitControls※1の回転機能をロックしてメッシュを動かしやすくする。
について
OrbitControlsの回転操作可否をDragControls の onDragStart と onDragEndで制御するようにしました。
meshInfo.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
import { Euler, Vector3 } from "@react-three/fiber"; export type MeshInfo = { position: Vector3; rotation: Euler; color: string; }; export const getSampleMeshInfos = (): MeshInfo[] => { const meshInfos: MeshInfo[] = [ { position: [0, 0, 0.5], rotation: [0, 0, 0], color: "red", }, { position: [0, 0, -0.5], rotation: [0, Math.PI, 0], color: "blue", }, { position: [-0.5, 0, 0], rotation: [0, Math.PI / 2, 0], color: "green", }, { position: [0.5, 0, 0], rotation: [0, -Math.PI / 2, 0], color: "yellow", }, { position: [0, 0.5, 0], rotation: [Math.PI / 2, 0, 0], color: "purple", }, { position: [0, -0.5, 0], rotation: [-Math.PI / 2, 0, 0], color: "orange", }, ]; return meshInfos; }; |
page.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
"use client"; import React, { Suspense, useState } from "react"; import { Canvas } from "@react-three/fiber"; import { OrbitControls, Stats } from "@react-three/drei"; import { DraggableObjects } from "./draggableObjects"; const DreiDragControl = () => { const [enableRotate, setEnableRotate] = useState<boolean>(true); return ( <div style={{ width: "100vw", height: "100vh" }}> <Canvas camera={{ position: [0, 10, 8], fov: 50, aspect: window.innerWidth / window.innerHeight, near: 0.1, far: 2000, }} dpr={window.devicePixelRatio} shadows > <color attach="background" args={["#1e1e1e"]} /> <Stats /> <OrbitControls enableRotate={enableRotate} /> <Suspense fallback={null}> <DraggableObjects setEnableRotate={setEnableRotate} /> </Suspense> <axesHelper /> <gridHelper position={[0, 0.01, 0]} args={[10, 10, "red", "black"]} /> <ambientLight intensity={2.0} /> <pointLight position={[10, 10, 10]} /> </Canvas> </div> ); }; export default DreiDragControl; |
動作結果
まとめ(今後の課題)
今回作成したドラッグして動かせる機能は便利なのですが、方向を固定して動かせた方が思った位置に動かすことができそうなので改善したいと思います。
個人的な感想としてThree.jsをそのまま使う場合と比較してかなりシンプルに実装できたと思います。
その辺りは今後また執筆できたらと思います。
今後も便利そうな機能あったら紹介していきたいと思います。(TransformControls など)
追記: 将来的にはもっと機能を追加、使いやすくして以下のような感じのUIを目標にしています。
COMMENTS