【React Three Fiber】DraggableなMeshの実装

<!-- =========================================================== -->
# 概要
<!-- =========================================================== -->
React Three Fiberで、**ドラッグ可能なメッシュ**の実装方法をまとめました。

https://rgn91.csb.app/
![output(video-cutter-js.com).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/d3ed3362-0c4e-4ad9-f3a5-c8e4c79881b8.gif)

この記事は、以下のコースの受講に伴って、ドラッグ操作について内容を整理して改めてまとめたものです。

https://www.udemy.com/course/threejs-using-react/

:::note info
このコースは、少し情報（使用しているライブラリのバージョン）が古いです。
受講してみたい方で、<strong>最新のライブラリのバージョンを使用したい方</strong>、<strong>TypeScriptで実装したい方</strong>は、私が受講した時のリポジトリが役に立つかもしれないです。ご参考の際は、<strong>Read Me</strong>を読んでください。
:::

https://github.com/nemutas/udemy-r3f

<!-- =========================================================== -->
# 実装
<!-- =========================================================== -->
```.tsx:App.tsx
import React, { Suspense, useEffect, useRef, VFC } from 'react';
import * as THREE from 'three';
import { DragControls } from 'three-stdlib';
import { OrbitControls, Sphere, Stats, TorusKnot, useHelper, useTexture } from '@react-three/drei';
import { Canvas, useFrame, useThree } from '@react-three/fiber';

export const App: VFC = () => {
	return (
		<Canvas
			camera={{
				position: [0, 10, 8],
				fov: 50,
				aspect: window.innerWidth / window.innerHeight,
				near: 0.1,
				far: 2000
			}}
			dpr={window.devicePixelRatio}
			shadows>
			{/* canvas color */}
			<color attach="background" args={['#1e1e1e']} />
			{/* fps */}
			<Stats />
			{/* camera controller */}
			<OrbitControls attach="orbitControls" />
			{/* lights */}
			<Draggable>
				<PointLight />
			</Draggable>
			{/* objects */}
			<Suspense fallback={null}>
				<Draggable>
					<ToonTorusKnot textureName="threeTone" color="#22cefa" position={[1, 2, 0]} />
				</Draggable>
				<Draggable>
					<ToonTorusKnot textureName="fiveTone" color="#ea7600" position={[-1, 2, 0]} />
				</Draggable>
			</Suspense>
			{/* helper */}
			<axesHelper />
			<gridHelper position={[0, 0.01, 0]} args={[10, 10, 'red', 'black']} />
		</Canvas>
	)
}

// ==============================================
type ToonTorusKnotProps = {
	textureName: string
	color: string
	position: [number, number, number]
}

const ToonTorusKnot: VFC<ToonTorusKnotProps> = props => {
	const { textureName, color, position } = props
	const ref = useRef<THREE.Mesh>(null)

	const toneMap = useTexture(`/assets/textures/${textureName}.jpg`)
	toneMap.minFilter = THREE.NearestFilter
	toneMap.magFilter = THREE.NearestFilter

	useFrame(() => {
		ref.current!.rotation.x += 0.005
		ref.current!.rotation.y += 0.005
	})

	return (
		<TorusKnot ref={ref} args={[0.5, 0.2, 128, 128]} position={position}>
			<meshToonMaterial color={color} gradientMap={toneMap} />
		</TorusKnot>
	)
}

// ==============================================
const PointLight: VFC = () => {
	const lightRef = useRef<THREE.Light>()

	useHelper(lightRef, THREE.PointLightHelper, [0.5])

	return (
		<group position={[0, 5, 0]}>
			<Sphere scale={0.2}>
				<meshBasicMaterial color="#f3f3f3" />
			</Sphere>
			<pointLight
				ref={lightRef}
				intensity={1}
				shadow-mapSize-width={2048}
				shadow-mapSize-height={2048}
				castShadow
			/>
		</group>
	)
}

// ==============================================
type DraggableProps = {
	children: React.ReactNode
}

const Draggable: VFC<DraggableProps> = ({ children }) => {
	const ref = useRef<THREE.Group>(null)
	const { camera, gl, scene } = useThree()

	useEffect(() => {
		const controls = new DragControls(ref.current!.children, camera, gl.domElement)
		controls.transformGroup = true

		const orbitControls = (scene as any).orbitControls

		controls.addEventListener('dragstart', () => {
			orbitControls.enabled = false
		})
		controls.addEventListener('dragend', () => {
			orbitControls.enabled = true
		})
	}, [camera, gl.domElement, scene])

	return <group ref={ref}>{children}</group>
}
```
### Draggable Componentの実装
実装には[**DragControls**](https://threejs.org/docs/index.html?q=drag#examples/en/controls/DragControls)を使用します。

```.tsx
type DraggableProps = {
    children: React.ReactNode
}

const Draggable: VFC<DraggableProps> = ({ children }) => {
    const ref = useRef<THREE.Group>(null)
    const { camera, gl, scene } = useThree()

    useEffect(() => {
        const controls = new DragControls(ref.current!.children, camera, gl.domElement)
        controls.transformGroup = true

        // console.log(scene)
        const orbitControls = (scene as any).orbitControls

        controls.addEventListener('dragstart', () => {
            orbitControls.enabled = false
        })
        controls.addEventListener('dragend', () => {
            orbitControls.enabled = true
        })
    }, [camera, gl.domElement, scene])

    return <group ref={ref}>{children}</group>
}
```

インスタンスを作成するには、引数に以下の3つが必要です。

* objects: An array of draggable 3D objects.
* camera: The camera of the rendered scene.
* domElement: The HTML element used for event listeners.

この内、`camera`と`domElement`は、**useThreeフック**経由で取得することができます。

`objects`には、ドラッグさせたいオブジェクトの配列を渡します。配列なので、引数で渡されたコンポーネント（children）をgroup化して、それを引数に渡します。ドラッグは、配列で渡されたオブジェクトそれぞれについて適用されます。

ただし、ドラッグできるオブジェクトは、そもそもドラッグが可能なオブジェクトに限ります。今回の例で言えば、**Meshオブジェクトはドラッグ可能**で、**Lightオブジェクトはドラッグ不可**です。

groupに対してドラッグを適用したい場合は、`controls.transformGroup = true`を設定します。groupにドラッグ可能なオブジェクトが1つでも含まれていれば、group自体がドラッグされるようになります。

このため、PointLightオブジェクトはドラッグ不可ですが、PointLightオブジェクトと一緒に渡されているSphereジオメトリがドラッグ可能で、それらをまとめてgroup化しているので、結果的にPointLightオブジェクトもドラッグ操作で移動します。

#### orbitControlsの無効化
`orbitControls`を追加している場合、ドラッグ操作でカメラも移動してしまします。そのため、ドラッグをしているときはカメラの操作を無効にする必要があります。
orbitControlsオブジェクトは、sceneオブジェクトから取得することができます。ただし、orbitControls追加時に明示的に**attach**する必要があります。

```.tsx
<OrbitControls attach="orbitControls" />
```
```shell:console.log
Scene
//・・・
> orbitControls: OrbitControls {object: PerspectiveCamera, domElement: canvas, enabled: true, target: Vector3, minDistance: 0, …}
```
あとは、`dragstart`・`dragend`イベントで、orbitControlsのtrue/falseを切り替えます。

```.tsx
const orbitControls = (scene as any).orbitControls

controls.addEventListener('dragstart', () => {
	orbitControls.enabled = false
})
controls.addEventListener('dragend', () => {
	orbitControls.enabled = true
})
```

### MeshToonMaterial
meshのmaterialには、[MeshToonMaterial](https://threejs.org/docs/index.html?q=mesh#api/en/materials/MeshToonMaterial)を使用しています。

```.tsx
const ToonTorusKnot: VFC<ToonTorusKnotProps> = props => {
	const { textureName, color, position } = props
	const ref = useRef<THREE.Mesh>(null)

	const toneMap = useTexture(`/assets/textures/${textureName}.jpg`)
	toneMap.minFilter = THREE.NearestFilter
	toneMap.magFilter = THREE.NearestFilter

	useFrame(() => {
		ref.current!.rotation.x += 0.005
		ref.current!.rotation.y += 0.005
	})

	return (
		<TorusKnot ref={ref} args={[0.5, 0.2, 128, 128]} position={position}>
			<meshToonMaterial color={color} gradientMap={toneMap} />
		</TorusKnot>
	)
}
```

**gradientMap**を指定して、影を3段階、5段階にしています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/91edef30-7bd0-5c7c-f4c0-18a2628ce52d.png" width="600">
texture画像は、three.jsの公式サイトから拝借しました。ダウンロード方法は**おまけ**を参照してください。

### PointLight
ライトは、位置がわかりやすいようにSphereジオメトリも追加しています。また、Sphereジオメトリを追加したことでPointLightもドラッグすることができます。

```.tsx
const PointLight: VFC = () => {
	const lightRef = useRef<THREE.Light>()

	useHelper(lightRef, THREE.PointLightHelper, [0.5])

	return (
		<group position={[0, 5, 0]}>
			<Sphere scale={0.2}>
				<meshBasicMaterial color="#f3f3f3" />
			</Sphere>
			<pointLight
				ref={lightRef}
				intensity={1}
				shadow-mapSize-width={2048}
				shadow-mapSize-height={2048}
				castShadow
			/>
		</group>
	)
}
```
ライト自体（pointLight）が本当にドラッグできているのか確認するために、helperを追加しています。
**useHelper**を使用することで簡単に実装することができます。特に、第3引数（[0.5]）にはPointLightHelperのインスタンス生成時に渡す`sphereSize`を渡します。

* [useHelper](https://docs.pmnd.rs/drei/misc/use-helper)
* [PointLightHelper](https://threejs.org/docs/index.html?q=help#api/en/helpers/PointLightHelper)


<!-- =========================================================== -->
# 成果物
<!-- =========================================================== -->

https://codesandbox.io/s/draggable-mesh-rgn91

<!-- =========================================================== -->
# おまけ
<!-- =========================================================== -->
### Texture 画像のダウンロード方法
[MeshToonMaterial](https://threejs.org/docs/index.html?q=mesh#api/en/materials/MeshToonMaterial)のページから、新規Windowでサンプルを開きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/7cfa26d9-ba7b-84d6-a742-060512a5498b.jpeg" width="500">
[F12]を押して、ソースのテクスチャファイルを選択して、新しいタブで開きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/d1f51594-a794-5ce6-b4bb-923a0eb2dbfd.png" width="500">
画像が新規画面で表示されるので、保存します。